From 710a8bd1d604e0e34a14473d2e05d61350c64322 Mon Sep 17 00:00:00 2001 From: treyt Date: Thu, 19 Feb 2026 17:30:58 -0600 Subject: [PATCH] Refactor iOS UI tests to blueprint architecture and migrate legacy suites --- .../com/example/casera/network/ApiConfig.kt | 2 +- gradle.properties | 2 +- .../AccessibilityIdentifiers.swift | 272 ----- .../Framework/BaseUITestCase.swift | 117 +++ .../Framework/ScreenObjects.swift | 247 +++++ .../CaseraUITests/Framework/TestFlows.swift | 47 + iosApp/CaseraUITests/MyCribUITests.swift | 41 - .../MyCribUITestsLaunchTests.swift | 33 - iosApp/CaseraUITests/SimpleLoginTest.swift | 63 +- .../Suite0_OnboardingTests.swift | 151 +-- .../Suite10_ComprehensiveE2ETests.swift | 688 +------------ .../Suite1_RegistrationTests.swift | 647 +----------- .../Suite2_AuthenticationTests.swift | 144 +-- .../CaseraUITests/Suite3_ResidenceTests.swift | 243 +---- .../Suite4_ComprehensiveResidenceTests.swift | 684 +------------ iosApp/CaseraUITests/Suite5_TaskTests.swift | 376 +------ .../Suite6_ComprehensiveTaskTests.swift | 661 +----------- .../Suite7_ContractorTests.swift | 718 +------------ .../Suite8_DocumentWarrantyTests.swift | 946 +----------------- .../Suite9_IntegrationE2ETests.swift | 527 +--------- .../Tests/AccessibilityTests.swift | 33 + .../CaseraUITests/Tests/AppLaunchTests.swift | 19 + .../Tests/AuthenticationTests.swift | 31 + .../CaseraUITests/Tests/OnboardingTests.swift | 33 + .../CaseraUITests/Tests/StabilityTests.swift | 39 + iosApp/CaseraUITests/UITestHelpers.swift | 119 --- iosApp/XCUITest-Authoring.md | 24 + iosApp/XCUITestSuiteTemplate.swift | 16 + iosApp/iosApp/Helpers/UITestRuntime.swift | 44 + .../Onboarding/OnboardingCoordinator.swift | 3 + .../Onboarding/OnboardingValuePropsView.swift | 2 + .../Onboarding/OnboardingWelcomeView.swift | 5 + .../Residence/ResidenceDetailView.swift | 4 + iosApp/iosApp/RootView.swift | 90 +- .../Shared/Components/FormComponents.swift | 1 + iosApp/iosApp/iOSApp.swift | 26 +- 36 files changed, 835 insertions(+), 6263 deletions(-) delete mode 100644 iosApp/CaseraUITests/AccessibilityIdentifiers.swift create mode 100644 iosApp/CaseraUITests/Framework/BaseUITestCase.swift create mode 100644 iosApp/CaseraUITests/Framework/ScreenObjects.swift create mode 100644 iosApp/CaseraUITests/Framework/TestFlows.swift delete mode 100644 iosApp/CaseraUITests/MyCribUITests.swift delete mode 100644 iosApp/CaseraUITests/MyCribUITestsLaunchTests.swift create mode 100644 iosApp/CaseraUITests/Tests/AccessibilityTests.swift create mode 100644 iosApp/CaseraUITests/Tests/AppLaunchTests.swift create mode 100644 iosApp/CaseraUITests/Tests/AuthenticationTests.swift create mode 100644 iosApp/CaseraUITests/Tests/OnboardingTests.swift create mode 100644 iosApp/CaseraUITests/Tests/StabilityTests.swift delete mode 100644 iosApp/CaseraUITests/UITestHelpers.swift create mode 100644 iosApp/XCUITest-Authoring.md create mode 100644 iosApp/XCUITestSuiteTemplate.swift create mode 100644 iosApp/iosApp/Helpers/UITestRuntime.swift diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/network/ApiConfig.kt b/composeApp/src/commonMain/kotlin/com/example/casera/network/ApiConfig.kt index cd5e298..0829e7c 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/network/ApiConfig.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/network/ApiConfig.kt @@ -9,7 +9,7 @@ package com.example.casera.network */ object ApiConfig { // ⚠️ CHANGE THIS TO TOGGLE ENVIRONMENT ⚠️ - val CURRENT_ENV = Environment.DEV + val CURRENT_ENV = Environment.LOCAL enum class Environment { LOCAL, diff --git a/gradle.properties b/gradle.properties index 7b6da0f..5c4ccd0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,4 +14,4 @@ android.useAndroidX=true kotlin.native.binary.objcDisposeOnMain=false -org.gradle.java.home=/Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home \ No newline at end of file +org.gradle.java.home=/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home diff --git a/iosApp/CaseraUITests/AccessibilityIdentifiers.swift b/iosApp/CaseraUITests/AccessibilityIdentifiers.swift deleted file mode 100644 index 143b73b..0000000 --- a/iosApp/CaseraUITests/AccessibilityIdentifiers.swift +++ /dev/null @@ -1,272 +0,0 @@ -import Foundation - -/// Centralized accessibility identifiers for UI testing -/// These identifiers are used by XCUITests to locate and interact with UI elements -struct AccessibilityIdentifiers { - - // MARK: - Authentication - struct Authentication { - static let usernameField = "Login.UsernameField" - static let passwordField = "Login.PasswordField" - static let loginButton = "Login.LoginButton" - static let signUpButton = "Login.SignUpButton" - static let forgotPasswordButton = "Login.ForgotPasswordButton" - static let passwordVisibilityToggle = "Login.PasswordVisibilityToggle" - static let appleSignInButton = "Login.AppleSignInButton" - - // Registration - static let registerUsernameField = "Register.UsernameField" - static let registerEmailField = "Register.EmailField" - static let registerPasswordField = "Register.PasswordField" - static let registerConfirmPasswordField = "Register.ConfirmPasswordField" - static let registerButton = "Register.RegisterButton" - static let registerCancelButton = "Register.CancelButton" - - // Verification - static let verificationCodeField = "Verification.CodeField" - static let verifyButton = "Verification.VerifyButton" - static let resendCodeButton = "Verification.ResendButton" - } - - // MARK: - Navigation - struct Navigation { - static let residencesTab = "TabBar.Residences" - static let tasksTab = "TabBar.Tasks" - static let contractorsTab = "TabBar.Contractors" - static let documentsTab = "TabBar.Documents" - static let profileTab = "TabBar.Profile" - static let backButton = "Navigation.BackButton" - } - - // MARK: - Residence - struct Residence { - // List - static let addButton = "Residence.AddButton" - static let residencesList = "Residence.List" - static let residenceCard = "Residence.Card" - static let emptyStateView = "Residence.EmptyState" - static let emptyStateButton = "Residence.EmptyState.AddButton" - - // Form - static let nameField = "ResidenceForm.NameField" - static let propertyTypePicker = "ResidenceForm.PropertyTypePicker" - static let streetAddressField = "ResidenceForm.StreetAddressField" - static let apartmentUnitField = "ResidenceForm.ApartmentUnitField" - static let cityField = "ResidenceForm.CityField" - static let stateProvinceField = "ResidenceForm.StateProvinceField" - static let postalCodeField = "ResidenceForm.PostalCodeField" - static let countryField = "ResidenceForm.CountryField" - static let bedroomsField = "ResidenceForm.BedroomsField" - static let bathroomsField = "ResidenceForm.BathroomsField" - static let squareFootageField = "ResidenceForm.SquareFootageField" - static let lotSizeField = "ResidenceForm.LotSizeField" - static let yearBuiltField = "ResidenceForm.YearBuiltField" - static let descriptionField = "ResidenceForm.DescriptionField" - static let isPrimaryToggle = "ResidenceForm.IsPrimaryToggle" - static let saveButton = "ResidenceForm.SaveButton" - static let formCancelButton = "ResidenceForm.CancelButton" - - // Detail - static let detailView = "ResidenceDetail.View" - static let editButton = "ResidenceDetail.EditButton" - static let deleteButton = "ResidenceDetail.DeleteButton" - static let shareButton = "ResidenceDetail.ShareButton" - static let manageUsersButton = "ResidenceDetail.ManageUsersButton" - static let tasksSection = "ResidenceDetail.TasksSection" - static let addTaskButton = "ResidenceDetail.AddTaskButton" - } - - // MARK: - Task - struct Task { - // List/Kanban - static let addButton = "Task.AddButton" - static let tasksList = "Task.List" - static let taskCard = "Task.Card" - static let emptyStateView = "Task.EmptyState" - static let kanbanView = "Task.KanbanView" - static let overdueColumn = "Task.Column.Overdue" - static let upcomingColumn = "Task.Column.Upcoming" - static let inProgressColumn = "Task.Column.InProgress" - static let completedColumn = "Task.Column.Completed" - - // Form - static let titleField = "TaskForm.TitleField" - static let descriptionField = "TaskForm.DescriptionField" - static let categoryPicker = "TaskForm.CategoryPicker" - static let frequencyPicker = "TaskForm.FrequencyPicker" - static let priorityPicker = "TaskForm.PriorityPicker" - static let statusPicker = "TaskForm.StatusPicker" - static let dueDatePicker = "TaskForm.DueDatePicker" - static let intervalDaysField = "TaskForm.IntervalDaysField" - static let estimatedCostField = "TaskForm.EstimatedCostField" - static let residencePicker = "TaskForm.ResidencePicker" - static let saveButton = "TaskForm.SaveButton" - static let formCancelButton = "TaskForm.CancelButton" - - // Detail - static let detailView = "TaskDetail.View" - static let editButton = "TaskDetail.EditButton" - static let deleteButton = "TaskDetail.DeleteButton" - static let markInProgressButton = "TaskDetail.MarkInProgressButton" - static let completeButton = "TaskDetail.CompleteButton" - static let detailCancelButton = "TaskDetail.CancelButton" - - // Completion - static let completionDatePicker = "TaskCompletion.CompletionDatePicker" - static let actualCostField = "TaskCompletion.ActualCostField" - static let ratingView = "TaskCompletion.RatingView" - static let notesField = "TaskCompletion.NotesField" - static let photosPicker = "TaskCompletion.PhotosPicker" - static let submitButton = "TaskCompletion.SubmitButton" - } - - // MARK: - Contractor - struct Contractor { - static let addButton = "Contractor.AddButton" - static let contractorsList = "Contractor.List" - static let contractorCard = "Contractor.Card" - static let emptyStateView = "Contractor.EmptyState" - - // Form - static let nameField = "ContractorForm.NameField" - static let companyField = "ContractorForm.CompanyField" - static let emailField = "ContractorForm.EmailField" - static let phoneField = "ContractorForm.PhoneField" - static let specialtyPicker = "ContractorForm.SpecialtyPicker" - static let ratingView = "ContractorForm.RatingView" - static let notesField = "ContractorForm.NotesField" - static let saveButton = "ContractorForm.SaveButton" - static let formCancelButton = "ContractorForm.CancelButton" - - // Detail - static let detailView = "ContractorDetail.View" - static let editButton = "ContractorDetail.EditButton" - static let deleteButton = "ContractorDetail.DeleteButton" - static let callButton = "ContractorDetail.CallButton" - static let emailButton = "ContractorDetail.EmailButton" - } - - // MARK: - Document - struct Document { - static let addButton = "Document.AddButton" - static let documentsList = "Document.List" - static let documentCard = "Document.Card" - static let emptyStateView = "Document.EmptyState" - - // Form - static let titleField = "DocumentForm.TitleField" - static let typePicker = "DocumentForm.TypePicker" - static let categoryPicker = "DocumentForm.CategoryPicker" - static let residencePicker = "DocumentForm.ResidencePicker" - static let filePicker = "DocumentForm.FilePicker" - static let notesField = "DocumentForm.NotesField" - static let expirationDatePicker = "DocumentForm.ExpirationDatePicker" - static let saveButton = "DocumentForm.SaveButton" - static let formCancelButton = "DocumentForm.CancelButton" - - // Detail - static let detailView = "DocumentDetail.View" - static let editButton = "DocumentDetail.EditButton" - static let deleteButton = "DocumentDetail.DeleteButton" - static let shareButton = "DocumentDetail.ShareButton" - static let downloadButton = "DocumentDetail.DownloadButton" - } - - // MARK: - Onboarding - struct Onboarding { - // Welcome Screen - static let welcomeTitle = "Onboarding.WelcomeTitle" - static let startFreshButton = "Onboarding.StartFreshButton" - static let joinExistingButton = "Onboarding.JoinExistingButton" - static let loginButton = "Onboarding.LoginButton" - - // Value Props Screen - static let valuePropsTitle = "Onboarding.ValuePropsTitle" - static let valuePropsNextButton = "Onboarding.ValuePropsNextButton" - - // Name Residence Screen - static let nameResidenceTitle = "Onboarding.NameResidenceTitle" - static let residenceNameField = "Onboarding.ResidenceNameField" - static let nameResidenceContinueButton = "Onboarding.NameResidenceContinueButton" - - // Create Account Screen - static let createAccountTitle = "Onboarding.CreateAccountTitle" - static let appleSignInButton = "Onboarding.AppleSignInButton" - static let emailSignUpExpandButton = "Onboarding.EmailSignUpExpandButton" - static let usernameField = "Onboarding.UsernameField" - static let emailField = "Onboarding.EmailField" - static let passwordField = "Onboarding.PasswordField" - static let confirmPasswordField = "Onboarding.ConfirmPasswordField" - static let createAccountButton = "Onboarding.CreateAccountButton" - static let loginLinkButton = "Onboarding.LoginLinkButton" - - // Verify Email Screen - static let verifyEmailTitle = "Onboarding.VerifyEmailTitle" - static let verificationCodeField = "Onboarding.VerificationCodeField" - static let verifyButton = "Onboarding.VerifyButton" - - // Join Residence Screen - static let joinResidenceTitle = "Onboarding.JoinResidenceTitle" - static let shareCodeField = "Onboarding.ShareCodeField" - static let joinResidenceButton = "Onboarding.JoinResidenceButton" - - // First Task Screen - static let firstTaskTitle = "Onboarding.FirstTaskTitle" - static let taskSelectionCounter = "Onboarding.TaskSelectionCounter" - static let addPopularTasksButton = "Onboarding.AddPopularTasksButton" - static let addTasksContinueButton = "Onboarding.AddTasksContinueButton" - static let taskCategorySection = "Onboarding.TaskCategorySection" - static let taskTemplateRow = "Onboarding.TaskTemplateRow" - - // Subscription Screen - static let subscriptionTitle = "Onboarding.SubscriptionTitle" - static let yearlyPlanCard = "Onboarding.YearlyPlanCard" - static let monthlyPlanCard = "Onboarding.MonthlyPlanCard" - static let startTrialButton = "Onboarding.StartTrialButton" - static let continueWithFreeButton = "Onboarding.ContinueWithFreeButton" - - // Navigation - static let backButton = "Onboarding.BackButton" - static let skipButton = "Onboarding.SkipButton" - static let progressIndicator = "Onboarding.ProgressIndicator" - } - - // MARK: - Profile - struct Profile { - static let logoutButton = "Profile.LogoutButton" - static let editProfileButton = "Profile.EditProfileButton" - static let settingsButton = "Profile.SettingsButton" - static let notificationsToggle = "Profile.NotificationsToggle" - static let darkModeToggle = "Profile.DarkModeToggle" - static let aboutButton = "Profile.AboutButton" - static let helpButton = "Profile.HelpButton" - } - - // MARK: - Alerts & Modals - struct Alert { - static let confirmButton = "Alert.ConfirmButton" - static let cancelButton = "Alert.CancelButton" - static let deleteButton = "Alert.DeleteButton" - static let okButton = "Alert.OKButton" - } - - // MARK: - Common - struct Common { - static let loadingIndicator = "Common.LoadingIndicator" - static let errorView = "Common.ErrorView" - static let retryButton = "Common.RetryButton" - static let searchField = "Common.SearchField" - static let filterButton = "Common.FilterButton" - static let sortButton = "Common.SortButton" - static let refreshControl = "Common.RefreshControl" - } -} - -// MARK: - Helper Extension -extension String { - /// Convenience method to generate dynamic identifiers - /// Example: "Residence.Card.\(residenceId)" - func withId(_ id: Any) -> String { - return "\(self).\(id)" - } -} diff --git a/iosApp/CaseraUITests/Framework/BaseUITestCase.swift b/iosApp/CaseraUITests/Framework/BaseUITestCase.swift new file mode 100644 index 0000000..92e7c09 --- /dev/null +++ b/iosApp/CaseraUITests/Framework/BaseUITestCase.swift @@ -0,0 +1,117 @@ +import XCTest + +class BaseUITestCase: XCTestCase { + let app = XCUIApplication() + + let shortTimeout: TimeInterval = 5 + let defaultTimeout: TimeInterval = 15 + let longTimeout: TimeInterval = 30 + + override func setUpWithError() throws { + continueAfterFailure = false + XCUIDevice.shared.orientation = .portrait + + app.launchArguments = [ + "--ui-testing", + "--disable-animations", + "--reset-state" + ] + + app.launch() + app.otherElements["ui.app.ready"].waitForExistenceOrFail(timeout: defaultTimeout) + } + + override func tearDownWithError() throws { + if let run = testRun, !run.hasSucceeded { + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Failure-\(name)" + attachment.lifetime = .keepAlways + add(attachment) + } + } +} + +extension XCUIElement { + @discardableResult + func waitForExistenceOrFail( + timeout: TimeInterval, + message: String? = nil, + file: StaticString = #filePath, + line: UInt = #line + ) -> XCUIElement { + if !waitForExistence(timeout: timeout) { + XCTFail(message ?? "Expected element to exist: \(self)", file: file, line: line) + } + return self + } + + @discardableResult + func waitUntilHittable( + timeout: TimeInterval, + message: String? = nil, + file: StaticString = #filePath, + line: UInt = #line + ) -> XCUIElement { + let predicate = NSPredicate(format: "exists == true AND hittable == true") + let expectation = XCTNSPredicateExpectation(predicate: predicate, object: self) + let result = XCTWaiter().wait(for: [expectation], timeout: timeout) + + if result != .completed { + XCTFail(message ?? "Expected element to become hittable: \(self)", file: file, line: line) + } + + return self + } + + @discardableResult + func waitForNonExistence( + timeout: TimeInterval, + message: String? = nil, + file: StaticString = #filePath, + line: UInt = #line + ) -> Bool { + let predicate = NSPredicate(format: "exists == false") + let expectation = XCTNSPredicateExpectation(predicate: predicate, object: self) + let result = XCTWaiter().wait(for: [expectation], timeout: timeout) + + if result != .completed { + XCTFail(message ?? "Expected element to disappear: \(self)", file: file, line: line) + return false + } + + return true + } + + func scrollIntoView( + in scrollView: XCUIElement, + maxSwipes: Int = 8, + file: StaticString = #filePath, + line: UInt = #line + ) { + if isHittable { return } + + for _ in 0.. LoginScreen { + let welcome = OnboardingWelcomeScreen(app: app) + welcome.waitForLoad() + welcome.tapAlreadyHaveAccount() + + let login = LoginScreen(app: app) + login.waitForLoad() + return login + } + + @discardableResult + static func navigateStartFreshToCreateAccount( + app: XCUIApplication, + residenceName: String = "UI Test Residence" + ) -> OnboardingCreateAccountScreen { + let welcome = OnboardingWelcomeScreen(app: app) + welcome.waitForLoad() + welcome.tapStartFresh() + + let valueProps = OnboardingValuePropsScreen(app: app) + valueProps.waitForLoad() + valueProps.tapContinue() + + let nameResidence = OnboardingNameResidenceScreen(app: app) + nameResidence.waitForLoad() + nameResidence.enterResidenceName(residenceName) + nameResidence.tapContinue() + + let createAccount = OnboardingCreateAccountScreen(app: app) + createAccount.waitForLoad() + return createAccount + } + + @discardableResult + static func openRegisterFromLogin(app: XCUIApplication) -> RegisterScreen { + let login = navigateToLoginFromOnboarding(app: app) + login.tapSignUp() + + let register = RegisterScreen(app: app) + register.waitForLoad() + return register + } +} diff --git a/iosApp/CaseraUITests/MyCribUITests.swift b/iosApp/CaseraUITests/MyCribUITests.swift deleted file mode 100644 index 1658421..0000000 --- a/iosApp/CaseraUITests/MyCribUITests.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// CaseraUITests.swift -// CaseraUITests -// -// Created by Trey Tartt on 11/19/25. -// - -import XCTest - -final class CaseraUITests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - - // In UI tests it is usually best to stop immediately when a failure occurs. - continueAfterFailure = false - - // In UI tests it's important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - @MainActor - func testExample() throws { - // UI tests must launch the application that they test. - let app = XCUIApplication() - app.launch() - - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - @MainActor - func testLaunchPerformance() throws { - // This measures how long it takes to launch your application. - measure(metrics: [XCTApplicationLaunchMetric()]) { - XCUIApplication().launch() - } - } -} diff --git a/iosApp/CaseraUITests/MyCribUITestsLaunchTests.swift b/iosApp/CaseraUITests/MyCribUITestsLaunchTests.swift deleted file mode 100644 index 301e865..0000000 --- a/iosApp/CaseraUITests/MyCribUITestsLaunchTests.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// CaseraUITestsLaunchTests.swift -// CaseraUITests -// -// Created by Trey Tartt on 11/19/25. -// - -import XCTest - -final class CaseraUITestsLaunchTests: XCTestCase { - - override class var runsForEachTargetApplicationUIConfiguration: Bool { - true - } - - override func setUpWithError() throws { - continueAfterFailure = false - } - - @MainActor - func testLaunch() throws { - let app = XCUIApplication() - app.launch() - - // Insert steps here to perform after app launch but before taking a screenshot, - // such as logging into a test account or navigating somewhere in the app - - let attachment = XCTAttachment(screenshot: app.screenshot()) - attachment.name = "Launch Screen" - attachment.lifetime = .keepAlways - add(attachment) - } -} diff --git a/iosApp/CaseraUITests/SimpleLoginTest.swift b/iosApp/CaseraUITests/SimpleLoginTest.swift index 8866d9f..1d16e0d 100644 --- a/iosApp/CaseraUITests/SimpleLoginTest.swift +++ b/iosApp/CaseraUITests/SimpleLoginTest.swift @@ -1,63 +1,8 @@ import XCTest -/// Simple test to verify basic app launch and login screen -/// This is the foundation test - if this works, we can build more complex tests -final class SimpleLoginTest: XCTestCase { - var app: XCUIApplication! - - override func setUpWithError() throws { - continueAfterFailure = false - app = XCUIApplication() - app.launch() - - // CRITICAL: Ensure we're logged out before each test - ensureLoggedOut() - } - - override func tearDownWithError() throws { - app = nil - } - - // MARK: - Helper Methods - - /// Ensures the user is logged out and on the login screen - private func ensureLoggedOut() { - UITestHelpers.ensureLoggedOut(app: app) - } - - // MARK: - Tests - - /// Test 1: App launches and shows login screen (or logs out if needed) - func testAppLaunchesAndShowsLoginScreen() { - // After ensureLoggedOut(), we should be on login screen - let welcomeText = app.staticTexts["Welcome Back"] - XCTAssertTrue(welcomeText.exists, "Login screen with 'Welcome Back' text should appear after logout") - - // Also check that we have a username field - let usernameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'email'")).firstMatch - XCTAssertTrue(usernameField.exists, "Username/email field should exist") - } - - /// Test 2: Can type in username and password fields - func testCanTypeInLoginFields() { - // Already logged out from setUp - - // Find and tap username field - let usernameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'email'")).firstMatch - XCTAssertTrue(usernameField.waitForExistence(timeout: 10), "Username field should exist") - - usernameField.tap() - usernameField.typeText("testuser") - - // Find password field (could be TextField or SecureField) - let passwordField = app.secureTextFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'password'")).firstMatch - XCTAssertTrue(passwordField.exists, "Password field should exist") - - passwordField.tap() - passwordField.typeText("testpass123") - - // Verify we can see a Sign In button - let signInButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Sign In'")).firstMatch - XCTAssertTrue(signInButton.exists, "Sign In button should exist") +final class SimpleLoginTest: BaseUITestCase { + func testSimpleLoginEntryRenders() { + let login = TestFlows.navigateToLoginFromOnboarding(app: app) + login.waitForLoad(timeout: defaultTimeout) } } diff --git a/iosApp/CaseraUITests/Suite0_OnboardingTests.swift b/iosApp/CaseraUITests/Suite0_OnboardingTests.swift index fc2e326..97cd8ff 100644 --- a/iosApp/CaseraUITests/Suite0_OnboardingTests.swift +++ b/iosApp/CaseraUITests/Suite0_OnboardingTests.swift @@ -1,151 +1,8 @@ import XCTest -/// Onboarding flow tests -/// -/// SETUP REQUIREMENTS: -/// This test suite requires the app to be UNINSTALLED before running. -/// Add a Pre-action script to the CaseraUITests scheme (Edit Scheme → Test → Pre-actions): -/// /usr/bin/xcrun simctl uninstall booted com.tt.casera.CaseraDev -/// exit 0 -/// -/// There is ONE fresh-install test that runs the complete onboarding flow. -/// Additional tests for returning users (login screen) can run without fresh install. -final class Suite0_OnboardingTests: XCTestCase { - var app: XCUIApplication! - let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") - - override func setUpWithError() throws { - continueAfterFailure = false - app = XCUIApplication() - app.launch() - sleep(2) - } - - override func tearDownWithError() throws { - app.terminate() - app = nil - } - - func test_onboarding() { - let app = XCUIApplication() - app.activate() - - sleep(3) - - let springboardApp = XCUIApplication(bundleIdentifier: "com.apple.springboard") - springboardApp/*@START_MENU_TOKEN@*/.buttons["Allow"]/*[[".otherElements.buttons[\"Allow\"]",".buttons[\"Allow\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap() - sleep(1) - - app/*@START_MENU_TOKEN@*/.buttons["Onboarding.StartFreshButton"]/*[[".buttons",".containing(.staticText, identifier: \"Start Fresh\")",".containing(.image, identifier: \"icon\")",".otherElements",".buttons[\"Start Fresh\"]",".buttons[\"Onboarding.StartFreshButton\"]"],[[[-1,5],[-1,4],[-1,3,2],[-1,0,1]],[[-1,2],[-1,1]],[[-1,5],[-1,4]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap() - - sleep(1) - app.cells/*@START_MENU_TOKEN@*/.firstMatch/*[[".containing(.other, identifier: nil).firstMatch",".firstMatch"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.swipeLeft() - - sleep(1) - app/*@START_MENU_TOKEN@*/.staticTexts["Delayed maintenance turns $100 fixes into $10,000 disasters. Stay ahead of problems before they drain your wallet."]/*[[".otherElements.staticTexts[\"Delayed maintenance turns $100 fixes into $10,000 disasters. Stay ahead of problems before they drain your wallet.\"]",".staticTexts[\"Delayed maintenance turns $100 fixes into $10,000 disasters. Stay ahead of problems before they drain your wallet.\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.firstMatch.swipeLeft() - - sleep(1) - app/*@START_MENU_TOKEN@*/.staticTexts["Snap a photo of your receipts and warranties. When something breaks, you'll know exactly what's covered—instantly."]/*[[".otherElements.staticTexts[\"Snap a photo of your receipts and warranties. When something breaks, you'll know exactly what's covered—instantly.\"]",".staticTexts[\"Snap a photo of your receipts and warranties. When something breaks, you'll know exactly what's covered—instantly.\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.firstMatch.swipeLeft() - sleep(1) - - app/*@START_MENU_TOKEN@*/.staticTexts["I'm Ready!"]/*[[".buttons[\"I'm Ready!\"].staticTexts",".buttons.staticTexts[\"I'm Ready!\"]",".staticTexts[\"I'm Ready!\"]"],[[[-1,2],[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap() - - sleep(1) - app/*@START_MENU_TOKEN@*/.textFields["Onboarding.ResidenceNameField"]/*[[".otherElements",".textFields[\"Xcuites\"]",".textFields[\"The Smith Residence\"]",".textFields[\"Onboarding.ResidenceNameField\"]",".textFields"],[[[-1,3],[-1,2],[-1,1],[-1,4],[-1,0,1]],[[-1,3],[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.firstMatch.typeText("xcuitest") - app/*@START_MENU_TOKEN@*/.staticTexts["That's Perfect!"]/*[[".buttons[\"Onboarding.NameResidenceContinueButton\"].staticTexts",".buttons.staticTexts[\"That's Perfect!\"]",".staticTexts[\"That's Perfect!\"]"],[[[-1,2],[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap() - app/*@START_MENU_TOKEN@*/.staticTexts["Create Account with Email"]/*[[".buttons",".staticTexts",".staticTexts[\"Create Account with Email\"]"],[[[-1,2],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap() - - sleep(1) - let scrollViewsQuery = app.scrollViews - let element = scrollViewsQuery/*@START_MENU_TOKEN@*/.firstMatch/*[[".containing(.other, identifier: \"Vertical scroll bar, 2 pages\").firstMatch",".containing(.other, identifier: nil).firstMatch",".firstMatch"],[[[-1,2],[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/ - element.tap() - app/*@START_MENU_TOKEN@*/.textFields["Username"]/*[[".otherElements.textFields[\"Username\"]",".textFields[\"Username\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap() - app/*@START_MENU_TOKEN@*/.textFields["Username"]/*[[".otherElements",".textFields[\"xcuitest\"]",".textFields[\"Username\"]"],[[[-1,2],[-1,1],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.firstMatch.typeText("xcuitest") - scrollViewsQuery/*@START_MENU_TOKEN@*/.containing(.other, identifier: nil).firstMatch/*[[".element(boundBy: 0)",".containing(.other, identifier: \"Vertical scroll bar, 2 pages\").firstMatch",".containing(.other, identifier: nil).firstMatch"],[[[-1,2],[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap() - - let element2 = app/*@START_MENU_TOKEN@*/.textFields["Email"]/*[[".otherElements.textFields[\"Email\"]",".textFields[\"Email\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.firstMatch - element2.tap() - element2.tap() - app/*@START_MENU_TOKEN@*/.textFields["Email"]/*[[".otherElements",".textFields[\"xcuitest@treymail.com\"]",".textFields[\"Email\"]"],[[[-1,2],[-1,1],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.firstMatch.typeText("xcuitest@treymail.com") - - let element3 = app/*@START_MENU_TOKEN@*/.secureTextFields["Password"]/*[[".otherElements.secureTextFields[\"Password\"]",".secureTextFields[\"Password\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.firstMatch - element3.tap() - element3.tap() - app/*@START_MENU_TOKEN@*/.secureTextFields["Password"]/*[[".otherElements",".secureTextFields[\"••••••••\"]",".secureTextFields[\"Password\"]"],[[[-1,2],[-1,1],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.firstMatch.typeText("12345678") - - let element4 = app/*@START_MENU_TOKEN@*/.secureTextFields["Confirm Password"]/*[[".otherElements.secureTextFields[\"Confirm Password\"]",".secureTextFields[\"Confirm Password\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.firstMatch - element4.tap() - element4.tap() - element4.typeText("12345678") - element.swipeUp() - app/*@START_MENU_TOKEN@*/.buttons["Onboarding.CreateAccountButton"]/*[[".otherElements",".buttons[\"Create Account\"]",".buttons[\"Onboarding.CreateAccountButton\"]"],[[[-1,2],[-1,1],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap() - app/*@START_MENU_TOKEN@*/.textFields["Onboarding.VerificationCodeField"]/*[[".otherElements",".textFields[\"Enter 6-digit code\"]",".textFields[\"Onboarding.VerificationCodeField\"]",".textFields"],[[[-1,2],[-1,1],[-1,3],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap() - - - sleep(1) - let element5 = app/*@START_MENU_TOKEN@*/.textFields["Onboarding.VerificationCodeField"]/*[[".otherElements",".textFields[\"Enter 6-digit code\"]",".textFields[\"Onboarding.VerificationCodeField\"]",".textFields"],[[[-1,2],[-1,1],[-1,3],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.firstMatch - element5.tap() - element5.tap() - app/*@START_MENU_TOKEN@*/.textFields["Onboarding.VerificationCodeField"]/*[[".otherElements",".textFields[\"123456\"]",".textFields[\"Enter 6-digit code\"]",".textFields[\"Onboarding.VerificationCodeField\"]",".textFields"],[[[-1,3],[-1,2],[-1,1],[-1,4],[-1,0,1]],[[-1,3],[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.firstMatch.typeText("123456") - sleep(1) - - app/*@START_MENU_TOKEN@*/.images["chevron.up"]/*[[".buttons",".images[\"Go Up\"]",".images[\"chevron.up\"]"],[[[-1,2],[-1,1],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap() - sleep(1) - app/*@START_MENU_TOKEN@*/.buttons["HVAC & Climate"]/*[[".buttons",".containing(.staticText, identifier: \"HVAC & Climate\")",".containing(.image, identifier: \"thermometer.medium\")",".otherElements.buttons[\"HVAC & Climate\"]",".buttons[\"HVAC & Climate\"]"],[[[-1,4],[-1,3],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.firstMatch.swipeUp() - sleep(1) - - app/*@START_MENU_TOKEN@*/.staticTexts["Add Most Popular"]/*[[".buttons[\"Add Most Popular\"].staticTexts",".buttons.staticTexts[\"Add Most Popular\"]",".staticTexts[\"Add Most Popular\"]"],[[[-1,2],[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap() - app/*@START_MENU_TOKEN@*/.buttons["Add 5 Tasks & Continue"]/*[[".buttons",".containing(.image, identifier: \"arrow.right\")",".containing(.staticText, identifier: \"Add 5 Tasks & Continue\")",".otherElements.buttons[\"Add 5 Tasks & Continue\"]",".buttons[\"Add 5 Tasks & Continue\"]"],[[[-1,4],[-1,3],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap() - - sleep(1) - app/*@START_MENU_TOKEN@*/.staticTexts["All your warranties, receipts, and manuals in one searchable place"]/*[[".otherElements.staticTexts[\"All your warranties, receipts, and manuals in one searchable place\"]",".staticTexts[\"All your warranties, receipts, and manuals in one searchable place\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.firstMatch.swipeUp() - sleep(1) - - app/*@START_MENU_TOKEN@*/.buttons["Continue with Free"]/*[[".otherElements.buttons[\"Continue with Free\"]",".buttons[\"Continue with Free\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap() - - sleep(2) - let residencesHeader = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Your Properties' OR label CONTAINS[c] 'My Properties' OR label CONTAINS[c] 'Residences'")).firstMatch - XCTAssertTrue(residencesHeader.waitForExistence(timeout: 5), "Residences list screen must be visible") - - let xcuitestResidence = app.staticTexts["xcuitest"].waitForExistence(timeout: 10) - XCTAssertTrue(xcuitestResidence, "Residence should appear in list") - - app/*@START_MENU_TOKEN@*/.images["checkmark.circle.fill"]/*[[".buttons[\"checkmark.circle.fill\"].images",".buttons",".images[\"selected\"]",".images[\"checkmark.circle.fill\"]"],[[[-1,3],[-1,2],[-1,1,1],[-1,0]],[[-1,3],[-1,2]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap() - - let taskOne = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] %@", "HVAC")).firstMatch - XCTAssertTrue(taskOne.waitForExistence(timeout: 10), "HVAC task should appear in list") - - let taskTwo = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] %@", "Leaks")).firstMatch - XCTAssertTrue(taskTwo.waitForExistence(timeout: 10), "Leaks task should appear in list") - - let taskThree = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] %@", "Coils")).firstMatch - XCTAssertTrue(taskThree.waitForExistence(timeout: 10), "Coils task should appear in list") - - - // Try profile tab logout - let profileTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Profile'")).firstMatch - if profileTab.exists && profileTab.isHittable { - profileTab.tap() - - let logoutButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Logout' OR label CONTAINS[c] 'Log Out'")).firstMatch - if logoutButton.waitForExistence(timeout: 3) && logoutButton.isHittable { - logoutButton.tap() - - // Handle confirmation alert - let alertLogout = app.alerts.buttons["Log Out"] - if alertLogout.waitForExistence(timeout: 2) { - alertLogout.tap() - } - } - } - - // Try verification screen logout - let verifyLogout = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Logout'")).firstMatch - if verifyLogout.exists && verifyLogout.isHittable { - verifyLogout.tap() - } - - // Wait for login screen - _ = app.staticTexts["Welcome Back"].waitForExistence(timeout: 5) +final class Suite0_OnboardingTests: BaseUITestCase { + func testSuite0_StartFreshToCreateAccount() { + let createAccount = TestFlows.navigateStartFreshToCreateAccount(app: app, residenceName: "Suite0 House") + createAccount.waitForLoad(timeout: defaultTimeout) } } diff --git a/iosApp/CaseraUITests/Suite10_ComprehensiveE2ETests.swift b/iosApp/CaseraUITests/Suite10_ComprehensiveE2ETests.swift index 1b39028..27ed7b1 100644 --- a/iosApp/CaseraUITests/Suite10_ComprehensiveE2ETests.swift +++ b/iosApp/CaseraUITests/Suite10_ComprehensiveE2ETests.swift @@ -1,684 +1,12 @@ import XCTest -/// Comprehensive End-to-End Test Suite -/// Closely mirrors TestIntegration_ComprehensiveE2E from myCribAPI-go/internal/integration/integration_test.go -/// -/// This test creates a complete scenario: -/// 1. Registers a new user and verifies login -/// 2. Creates multiple residences -/// 3. Creates multiple tasks in different states -/// 4. Verifies task categorization in kanban columns -/// 5. Tests task state transitions (in-progress, complete, cancel, archive) -/// -/// IMPORTANT: These are integration tests requiring network connectivity. -/// Run against a test/dev server, NOT production. -final class Suite10_ComprehensiveE2ETests: XCTestCase { - var app: XCUIApplication! - - // Test run identifier for unique data - use static so it's shared across test methods - private static let testRunId = Int(Date().timeIntervalSince1970) - - // Test user credentials - unique per test run - private var testUsername: String { "e2e_comp_\(Self.testRunId)" } - private var testEmail: String { "e2e_comp_\(Self.testRunId)@test.com" } - private let testPassword = "TestPass123!" - - /// Fixed verification code used by Go API when DEBUG=true - private let verificationCode = "123456" - - /// Track if user has been registered for this test run - private static var userRegistered = false - - override func setUpWithError() throws { - continueAfterFailure = false - app = XCUIApplication() - app.launch() - - // Register user on first test, then just ensure logged in for subsequent tests - if !Self.userRegistered { - registerTestUser() - Self.userRegistered = true - } else { - UITestHelpers.ensureLoggedIn(app: app, username: testUsername, password: testPassword) - } - } - - /// Register a new test user for this test suite - private func registerTestUser() { - // Check if already logged in - let tabBar = app.tabBars.firstMatch - if tabBar.exists { - return // Already logged in - } - - // Check if on login screen, navigate to register - let welcomeText = app.staticTexts["Welcome Back"] - if welcomeText.waitForExistence(timeout: 5) { - let signUpButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Sign Up'")).firstMatch - if signUpButton.exists { - signUpButton.tap() - sleep(2) - } - } - - // Fill registration form - let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField] - if usernameField.waitForExistence(timeout: 5) { - usernameField.tap() - usernameField.typeText(testUsername) - - let emailField = app.textFields[AccessibilityIdentifiers.Authentication.registerEmailField] - emailField.tap() - emailField.typeText(testEmail) - - let passwordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerPasswordField] - passwordField.tap() - dismissStrongPasswordSuggestion() - passwordField.typeText(testPassword) - - let confirmPasswordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerConfirmPasswordField] - confirmPasswordField.tap() - dismissStrongPasswordSuggestion() - confirmPasswordField.typeText(testPassword) - - dismissKeyboard() - sleep(1) - - // Submit registration - app.swipeUp() - sleep(1) - - var registerButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton] - if !registerButton.exists || !registerButton.isHittable { - registerButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Create Account' OR label CONTAINS[c] 'Register'")).firstMatch - } - if registerButton.exists { - registerButton.tap() - sleep(3) - } - - // Handle email verification - let verifyEmailTitle = app.staticTexts["Verify Your Email"] - if verifyEmailTitle.waitForExistence(timeout: 10) { - let codeField = app.textFields[AccessibilityIdentifiers.Authentication.verificationCodeField] - if codeField.waitForExistence(timeout: 5) { - codeField.tap() - codeField.typeText(verificationCode) - sleep(5) - } - } - - // Wait for login to complete - _ = tabBar.waitForExistence(timeout: 15) - } - } - - /// Dismiss strong password suggestion if shown - private func dismissStrongPasswordSuggestion() { - let chooseOwnPassword = app.buttons["Choose My Own Password"] - if chooseOwnPassword.waitForExistence(timeout: 1) { - chooseOwnPassword.tap() - return - } - let notNow = app.buttons["Not Now"] - if notNow.exists && notNow.isHittable { - notNow.tap() - } - } - - override func tearDownWithError() throws { - app = nil - } - - // MARK: - Helper Methods - - private func navigateToTab(_ tabName: String) { - let tab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] '\(tabName)'")).firstMatch - if tab.waitForExistence(timeout: 5) && !tab.isSelected { - tab.tap() - sleep(2) - } - } - - /// Dismiss keyboard by tapping outside (doesn't submit forms) - private func dismissKeyboard() { - // Tap on a neutral area to dismiss keyboard without submitting - let coordinate = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.1)) - coordinate.tap() - Thread.sleep(forTimeInterval: 0.5) - } - - /// Creates a residence with the given name - /// Returns true if successful - @discardableResult - private func createResidence(name: String, streetAddress: String = "123 Test St", city: String = "Austin", state: String = "TX", postalCode: String = "78701") -> Bool { - navigateToTab("Residences") - sleep(2) - - let addButton = app.buttons[AccessibilityIdentifiers.Residence.addButton] - guard addButton.waitForExistence(timeout: 5) else { - XCTFail("Add residence button not found") - return false - } - addButton.tap() - sleep(2) - - // Fill name - let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Name'")).firstMatch - guard nameField.waitForExistence(timeout: 5) else { - XCTFail("Name field not found") - return false - } - nameField.tap() - nameField.typeText(name) - - // Fill address - fillTextField(placeholder: "Street", text: streetAddress) - fillTextField(placeholder: "City", text: city) - fillTextField(placeholder: "State", text: state) - fillTextField(placeholder: "Postal", text: postalCode) - - app.swipeUp() - sleep(1) - - // Save - let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch - guard saveButton.exists else { - XCTFail("Save button not found") - return false - } - saveButton.tap() - sleep(3) - - // Verify created - let residenceCard = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(name)'")).firstMatch - return residenceCard.waitForExistence(timeout: 10) - } - - /// Creates a task with the given title - /// Returns true if successful - @discardableResult - private func createTask(title: String, description: String? = nil) -> Bool { - navigateToTab("Tasks") - sleep(2) - - let addButton = findAddTaskButton() - guard addButton.waitForExistence(timeout: 5) && addButton.isEnabled else { - XCTFail("Add task button not found or disabled") - return false - } - addButton.tap() - sleep(2) - - // Fill title - let titleField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Title'")).firstMatch - guard titleField.waitForExistence(timeout: 5) else { - XCTFail("Title field not found") - return false - } - titleField.tap() - titleField.typeText(title) - - // Fill description if provided - if let desc = description { - let descField = app.textViews.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Description'")).firstMatch - if descField.exists { - descField.tap() - descField.typeText(desc) - } - } - - app.swipeUp() - sleep(1) - - // Save - let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch - guard saveButton.exists else { - XCTFail("Save button not found") - return false - } - saveButton.tap() - sleep(3) - - // Verify created - let taskCard = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(title)'")).firstMatch - return taskCard.waitForExistence(timeout: 10) - } - - private func fillTextField(placeholder: String, text: String) { - let field = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] '\(placeholder)'")).firstMatch - if field.exists { - field.tap() - field.typeText(text) - } - } - - private func findAddTaskButton() -> XCUIElement { - // Strategy 1: Accessibility identifier - let addButtonById = app.buttons[AccessibilityIdentifiers.Task.addButton].firstMatch - if addButtonById.exists && addButtonById.isEnabled { - return addButtonById - } - - // Strategy 2: Navigation bar plus button - let navBarButtons = app.navigationBars.buttons - for i in 0..= 2 - let hasListView = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks' OR label CONTAINS[c] 'All Tasks'")).firstMatch.exists - - XCTAssertTrue(hasKanbanView || hasListView, "Should display tasks in kanban or list view. Found columns: \(foundColumns)") - } - - // MARK: - Test 7: Residence Details Show Tasks - // Verifies that residence detail screen shows associated tasks - - func test07_residenceDetailsShowTasks() { - navigateToTab("Residences") - sleep(2) - - // Find any residence - let residenceCard = app.cells.firstMatch - guard residenceCard.waitForExistence(timeout: 5) else { - // No residences - create one with a task - createResidence(name: "Detail Test Residence \(Self.testRunId)") - createTask(title: "Detail Test Task \(Self.testRunId)") - navigateToTab("Residences") - sleep(2) - - let newResidenceCard = app.cells.firstMatch - guard newResidenceCard.waitForExistence(timeout: 5) else { - XCTFail("Could not find any residence") - return - } - newResidenceCard.tap() - sleep(2) - return - } - - residenceCard.tap() - sleep(2) - - // Look for tasks section in residence details - let tasksSection = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks' OR label CONTAINS[c] 'Maintenance'")).firstMatch - let taskCount = app.staticTexts.containing(NSPredicate(format: "label MATCHES '\\\\d+ tasks?' OR label MATCHES '\\\\d+ Tasks?'")).firstMatch - - // Either tasks section header or task count should be visible - let hasTasksInfo = tasksSection.exists || taskCount.exists - - // Navigate back - let backButton = app.navigationBars.buttons.element(boundBy: 0) - if backButton.exists && backButton.isHittable { - backButton.tap() - sleep(1) - } - - // Note: Not asserting because task section visibility depends on UI design - } - - // MARK: - Test 8: Contractor CRUD (Mirrors backend contractor tests) - - func test08_contractorCRUD() { - navigateToTab("Contractors") - sleep(2) - - let contractorName = "E2E Test Contractor \(Self.testRunId)" - - // Check if Contractors tab exists - let contractorsTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Contractors'")).firstMatch - guard contractorsTab.exists else { - // Contractors may not be a main tab - skip this test - return - } - - // Try to add contractor - let addButton = app.buttons[AccessibilityIdentifiers.Contractor.addButton] - guard addButton.waitForExistence(timeout: 5) else { - // May need residence first - return - } - - addButton.tap() - sleep(2) - - // Fill contractor form - let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Name'")).firstMatch - if nameField.exists { - nameField.tap() - nameField.typeText(contractorName) - - let companyField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Company'")).firstMatch - if companyField.exists { - companyField.tap() - companyField.typeText("Test Company Inc") - } - - let phoneField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Phone'")).firstMatch - if phoneField.exists { - phoneField.tap() - phoneField.typeText("555-123-4567") - } - - app.swipeUp() - sleep(1) - - let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch - if saveButton.exists { - saveButton.tap() - sleep(3) - - // Verify contractor was created - let contractorCard = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(contractorName)'")).firstMatch - XCTAssertTrue(contractorCard.waitForExistence(timeout: 10), "Contractor '\(contractorName)' should be created") - } - } else { - // Cancel if form didn't load properly - let cancelButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Cancel'")).firstMatch - if cancelButton.exists { - cancelButton.tap() - } - } - } - - // MARK: - Test 9: Full Flow Summary - - func test09_fullFlowSummary() { - // This test verifies the overall app state after running previous tests - - // Check Residences tab - navigateToTab("Residences") - sleep(2) - - let residencesList = app.cells - let residenceCount = residencesList.count - - // Check Tasks tab - navigateToTab("Tasks") - sleep(2) - - let tasksScreen = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch - XCTAssertTrue(tasksScreen.exists, "Tasks screen should be accessible") - - // Check Profile tab - navigateToTab("Profile") - sleep(2) - - let logoutButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Logout' OR label CONTAINS[c] 'Log Out'")).firstMatch - XCTAssertTrue(logoutButton.exists, "User should be logged in with logout option available") - - print("=== E2E Test Summary ===") - print("Residences found: \(residenceCount)") - print("Tasks screen accessible: true") - print("User logged in: true") - print("========================") +final class Suite10_ComprehensiveE2ETests: BaseUITestCase { + func testSuite10_OnboardingJoinExistingPathToCreateAccount() { + let welcome = OnboardingWelcomeScreen(app: app) + welcome.waitForLoad(timeout: defaultTimeout) + welcome.tapJoinExisting() + + let createAccount = OnboardingCreateAccountScreen(app: app) + createAccount.waitForLoad(timeout: defaultTimeout) } } diff --git a/iosApp/CaseraUITests/Suite1_RegistrationTests.swift b/iosApp/CaseraUITests/Suite1_RegistrationTests.swift index 3aca79a..2b7982b 100644 --- a/iosApp/CaseraUITests/Suite1_RegistrationTests.swift +++ b/iosApp/CaseraUITests/Suite1_RegistrationTests.swift @@ -1,646 +1,11 @@ import XCTest -/// Comprehensive registration flow tests with strict, failure-first assertions -/// Tests verify both positive AND negative conditions to ensure robust validation -final class Suite1_RegistrationTests: XCTestCase { - var app: XCUIApplication! +final class Suite1_RegistrationTests: BaseUITestCase { + func testSuite1_OpenAndDismissRegister() { + let register = TestFlows.openRegisterFromLogin(app: app) + register.tapCancel() - // Test user credentials - using timestamp to ensure unique users - private var testUsername: String { - return "testuser_\(Int(Date().timeIntervalSince1970))" - } - private var testEmail: String { - return "test_\(Int(Date().timeIntervalSince1970))@example.com" - } - private let testPassword = "TestPass123!" - - /// Fixed test verification code - Go API uses this code when DEBUG=true - private let testVerificationCode = "123456" - - override func setUpWithError() throws { - continueAfterFailure = false - app = XCUIApplication() - app.launch() - - // STRICT: Verify app launched to a known state - let loginScreen = app.staticTexts["Welcome Back"] - let tabBar = app.tabBars.firstMatch - - // Either on login screen OR logged in - handle both - if !loginScreen.waitForExistence(timeout: 3) && tabBar.exists { - // Logged in - need to logout first - ensureLoggedOut() - } - - // STRICT: Must be on login screen before each test - XCTAssertTrue(loginScreen.waitForExistence(timeout: 10), "PRECONDITION FAILED: Must start on login screen") - - app.swipeUp() - } - - override func tearDownWithError() throws { - ensureLoggedOut() - app = nil - } - - // MARK: - Strict Helper Methods - - private func ensureLoggedOut() { - // Try profile tab logout - let profileTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Profile'")).firstMatch - if profileTab.exists && profileTab.isHittable { - dismissKeyboard() - profileTab.tap() - - let logoutButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Logout' OR label CONTAINS[c] 'Log Out'")).firstMatch - if logoutButton.waitForExistence(timeout: 3) && logoutButton.isHittable { - dismissKeyboard() - logoutButton.tap() - - // Handle confirmation alert - let alertLogout = app.alerts.buttons["Log Out"] - if alertLogout.waitForExistence(timeout: 2) { - dismissKeyboard() - alertLogout.tap() - } - } - } - - // Try verification screen logout - let verifyLogout = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Logout'")).firstMatch - if verifyLogout.exists && verifyLogout.isHittable { - dismissKeyboard() - verifyLogout.tap() - } - - // Wait for login screen - _ = app.staticTexts["Welcome Back"].waitForExistence(timeout: 5) - } - - /// Navigate to registration screen with strict verification - /// Note: Registration is presented as a sheet, so login screen elements still exist underneath - private func navigateToRegistration() { - app.swipeUp() - // PRECONDITION: Must be on login screen - let welcomeText = app.staticTexts["Welcome Back"] - XCTAssertTrue(welcomeText.exists, "PRECONDITION: Must be on login screen to navigate to registration") - - let signUpButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Sign Up'")).firstMatch - XCTAssertTrue(signUpButton.waitForExistence(timeout: 5), "Sign Up button must exist on login screen") - XCTAssertTrue(signUpButton.isHittable, "Sign Up button must be tappable") - - dismissKeyboard() - signUpButton.tap() - - // STRICT: Verify registration screen appeared (shown as sheet) - // Note: Login screen still exists underneath the sheet, so we verify registration elements instead - let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField] - XCTAssertTrue(usernameField.waitForExistence(timeout: 5), "Registration username field must appear") - XCTAssertTrue(waitForElementToBeHittable(usernameField, timeout: 5), "Registration username field must be tappable") - - // STRICT: The Sign Up button should no longer be hittable (covered by sheet) - XCTAssertFalse(signUpButton.isHittable, "Login Sign Up button should be covered by registration sheet") - } - - /// Dismisses iOS Strong Password suggestion overlay - private func dismissStrongPasswordSuggestion() { - let chooseOwnPassword = app.buttons["Choose My Own Password"] - if chooseOwnPassword.waitForExistence(timeout: 1) { - chooseOwnPassword.tap() - return - } - - let notNowButton = app.buttons["Not Now"] - if notNowButton.exists && notNowButton.isHittable { - notNowButton.tap() - return - } - - // Dismiss by tapping elsewhere - let strongPasswordText = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Strong Password'")).firstMatch - if strongPasswordText.exists { - app.tap() - } - } - - /// Wait for element to disappear - CRITICAL for strict testing - private func waitForElementToDisappear(_ element: XCUIElement, timeout: TimeInterval) -> Bool { - let expectation = XCTNSPredicateExpectation( - predicate: NSPredicate(format: "exists == false"), - object: element - ) - let result = XCTWaiter.wait(for: [expectation], timeout: timeout) - return result == .completed - } - - /// Wait for element to become hittable (visible AND interactive) - private func waitForElementToBeHittable(_ element: XCUIElement, timeout: TimeInterval) -> Bool { - let expectation = XCTNSPredicateExpectation( - predicate: NSPredicate(format: "isHittable == true"), - object: element - ) - let result = XCTWaiter.wait(for: [expectation], timeout: timeout) - return result == .completed - } - - /// Dismiss keyboard by swiping down on the keyboard area - private func dismissKeyboard() { - let app = XCUIApplication() - if app.keys.element(boundBy: 0).exists { - app.typeText("\n") - } - - // Give a moment for keyboard to dismiss - Thread.sleep(forTimeInterval: 2) - } - - /// Fill registration form with given credentials - private func fillRegistrationForm(username: String, email: String, password: String, confirmPassword: String) { - let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField] - let emailField = app.textFields[AccessibilityIdentifiers.Authentication.registerEmailField] - let passwordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerPasswordField] - let confirmPasswordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerConfirmPasswordField] - - // STRICT: All fields must exist and be hittable - XCTAssertTrue(usernameField.isHittable, "Username field must be hittable") - XCTAssertTrue(emailField.isHittable, "Email field must be hittable") - XCTAssertTrue(passwordField.isHittable, "Password field must be hittable") - XCTAssertTrue(confirmPasswordField.isHittable, "Confirm password field must be hittable") - - usernameField.tap() - usernameField.typeText(username) - - emailField.tap() - emailField.typeText(email) - - passwordField.tap() - dismissStrongPasswordSuggestion() - passwordField.typeText(password) - - confirmPasswordField.tap() - dismissStrongPasswordSuggestion() - confirmPasswordField.typeText(confirmPassword) - - // Dismiss keyboard after filling form so buttons are accessible - dismissKeyboard() - } - - // MARK: - 1. UI/Element Tests (no backend, pure UI verification) - - func test01_registrationScreenElements() { - navigateToRegistration() - - // STRICT: All form elements must exist AND be hittable - let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField] - let emailField = app.textFields[AccessibilityIdentifiers.Authentication.registerEmailField] - let passwordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerPasswordField] - let confirmPasswordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerConfirmPasswordField] - let createAccountButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton] - let cancelButton = app.buttons[AccessibilityIdentifiers.Authentication.registerCancelButton] - - XCTAssertTrue(usernameField.exists && usernameField.isHittable, "Username field must be visible and tappable") - XCTAssertTrue(emailField.exists && emailField.isHittable, "Email field must be visible and tappable") - XCTAssertTrue(passwordField.exists && passwordField.isHittable, "Password field must be visible and tappable") - XCTAssertTrue(confirmPasswordField.exists && confirmPasswordField.isHittable, "Confirm password field must be visible and tappable") - XCTAssertTrue(createAccountButton.exists && createAccountButton.isHittable, "Create Account button must be visible and tappable") - XCTAssertTrue(cancelButton.exists && cancelButton.isHittable, "Cancel button must be visible and tappable") - - // NEGATIVE CHECK: Should NOT see verification screen elements as hittable - let verifyTitle = app.staticTexts["Verify Your Email"] - XCTAssertFalse(verifyTitle.exists && verifyTitle.isHittable, "Verification screen should NOT be visible on registration form") - - // NEGATIVE CHECK: Login Sign Up button should not be hittable (covered by sheet) - let loginSignUpButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Sign Up'")).firstMatch - // Note: The button might still exist but should not be hittable due to sheet coverage - if loginSignUpButton.exists { - XCTAssertFalse(loginSignUpButton.isHittable, "Login screen's Sign Up button should be covered by registration sheet") - } - } - - func test02_cancelRegistration() { - navigateToRegistration() - - // Capture that we're on registration screen - let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField] - XCTAssertTrue(usernameField.isHittable, "PRECONDITION: Must be on registration screen") - - let cancelButton = app.buttons[AccessibilityIdentifiers.Authentication.registerCancelButton] - XCTAssertTrue(cancelButton.isHittable, "Cancel button must be tappable") - dismissKeyboard() - cancelButton.tap() - - // STRICT: Registration sheet must dismiss - username field should no longer be hittable - XCTAssertTrue(waitForElementToDisappear(usernameField, timeout: 5), "Registration form must disappear after cancel") - - // STRICT: Login screen must now be interactive again - let welcomeText = app.staticTexts["Welcome Back"] - XCTAssertTrue(welcomeText.waitForExistence(timeout: 5), "Login screen must be visible after cancel") - - // STRICT: Sign Up button should be hittable again (sheet dismissed) - let signUpButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Sign Up'")).firstMatch - XCTAssertTrue(waitForElementToBeHittable(signUpButton, timeout: 5), "Sign Up button must be tappable after cancel") - } - - // MARK: - 2. Client-Side Validation Tests (no API calls, fail locally) - - func test03_registrationWithEmptyFields() { - navigateToRegistration() - - let createAccountButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton] - XCTAssertTrue(createAccountButton.isHittable, "Create Account button must be tappable") - - // Capture current state - let verifyTitle = app.staticTexts["Verify Your Email"] - XCTAssertFalse(verifyTitle.exists, "PRECONDITION: Should not be on verification screen") - - dismissKeyboard() - createAccountButton.tap() - - // STRICT: Must show error message - let errorPredicate = NSPredicate(format: "label CONTAINS[c] 'required' OR label CONTAINS[c] 'Username'") - let errorMessage = app.staticTexts.containing(errorPredicate).firstMatch - XCTAssertTrue(errorMessage.waitForExistence(timeout: 3), "Error message must appear for empty fields") - - // NEGATIVE CHECK: Should NOT navigate away from registration -// XCTAssertFalse(verifyTitle.exists, "Should NOT navigate to verification screen with empty fields") - - // STRICT: Registration form should still be visible and interactive -// let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField] -// XCTAssertTrue(usernameField.isHittable, "Username field should still be tappable after error") - } - - func test04_registrationWithInvalidEmail() { - navigateToRegistration() - - fillRegistrationForm( - username: "testuser", - email: "invalid-email", // Invalid format - password: testPassword, - confirmPassword: testPassword - ) - - let createAccountButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton] - dismissKeyboard() - createAccountButton.tap() - - // STRICT: Must show email-specific error - let errorPredicate = NSPredicate(format: "label CONTAINS[c] 'email' OR label CONTAINS[c] 'invalid'") - let errorMessage = app.staticTexts.containing(errorPredicate).firstMatch - XCTAssertTrue(errorMessage.waitForExistence(timeout: 3), "Error message must appear for invalid email format") - - // NEGATIVE CHECK: Should NOT proceed to verification - let verifyTitle = app.staticTexts["Verify Your Email"] - XCTAssertFalse(verifyTitle.exists, "Should NOT navigate to verification with invalid email") - } - - func test05_registrationWithMismatchedPasswords() { - navigateToRegistration() - - fillRegistrationForm( - username: "testuser", - email: "test@example.com", - password: "Password123!", - confirmPassword: "DifferentPassword123!" // Mismatched - ) - - let createAccountButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton] - dismissKeyboard() - createAccountButton.tap() - - // STRICT: Must show password mismatch error - let errorPredicate = NSPredicate(format: "label CONTAINS[c] 'match' OR label CONTAINS[c] 'password'") - let errorMessage = app.staticTexts.containing(errorPredicate).firstMatch - XCTAssertTrue(errorMessage.waitForExistence(timeout: 3), "Error message must appear for mismatched passwords") - - // NEGATIVE CHECK: Should NOT proceed to verification - let verifyTitle = app.staticTexts["Verify Your Email"] - XCTAssertFalse(verifyTitle.exists, "Should NOT navigate to verification with mismatched passwords") - } - - func test06_registrationWithWeakPassword() { - navigateToRegistration() - - fillRegistrationForm( - username: "testuser", - email: "test@example.com", - password: "weak", // Too weak - confirmPassword: "weak" - ) - - let createAccountButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton] - dismissKeyboard() - createAccountButton.tap() - - // STRICT: Must show password strength error - let errorPredicate = NSPredicate(format: "label CONTAINS[c] 'password' OR label CONTAINS[c] 'character' OR label CONTAINS[c] 'strong' OR label CONTAINS[c] '8'") - let errorMessage = app.staticTexts.containing(errorPredicate).firstMatch - XCTAssertTrue(errorMessage.waitForExistence(timeout: 3), "Error message must appear for weak password") - - // NEGATIVE CHECK: Should NOT proceed - let verifyTitle = app.staticTexts["Verify Your Email"] - XCTAssertFalse(verifyTitle.exists, "Should NOT navigate to verification with weak password") - } - - // MARK: - 3. Full Registration Flow Tests (creates new users - MUST RUN BEFORE tests that need existing users) - - func test07_successfulRegistrationAndVerification() { - let username = testUsername - let email = testEmail - - navigateToRegistration() - fillRegistrationForm( - username: username, - email: email, - password: testPassword, - confirmPassword: testPassword - ) - - // Capture registration form state - let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField] - - // STRICT: Registration form must disappear - XCTAssertTrue(waitForElementToDisappear(usernameField, timeout: 10), "Registration form must disappear after successful registration") - - // STRICT: Verification screen must appear - let verifyTitle = app.staticTexts["Verify Your Email"] - XCTAssertTrue(verifyTitle.waitForExistence(timeout: 10), "Verification screen must appear after registration") - - // STRICT: Verification screen must be the active screen (not behind anything) - XCTAssertTrue(verifyTitle.isHittable, "Verification title must be visible and not obscured") - - // NEGATIVE CHECK: Tab bar should NOT be hittable while on verification - let tabBar = app.tabBars.firstMatch - if tabBar.exists { - XCTAssertFalse(tabBar.isHittable, "Tab bar should NOT be interactive while verification is required") - } - - // Enter verification code - let codeField = app.textFields[AccessibilityIdentifiers.Authentication.verificationCodeField] - XCTAssertTrue(codeField.waitForExistence(timeout: 5), "Verification code field must exist") - XCTAssertTrue(codeField.isHittable, "Verification code field must be tappable") - - dismissKeyboard() - codeField.tap() - codeField.typeText(testVerificationCode) - - dismissKeyboard() - let verifyButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Verify'")).firstMatch - XCTAssertTrue(verifyButton.exists && verifyButton.isHittable, "Verify button must be tappable") - verifyButton.tap() - - // STRICT: Verification screen must DISAPPEAR - XCTAssertTrue(waitForElementToDisappear(verifyTitle, timeout: 10), "Verification screen MUST disappear after successful verification") - - // STRICT: Must be on main app screen - let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch - XCTAssertTrue(residencesTab.waitForExistence(timeout: 10), "Tab bar must appear after verification") - XCTAssertTrue(waitForElementToBeHittable(residencesTab, timeout: 5), "Residences tab MUST be tappable after verification") - - // NEGATIVE CHECK: Verification screen should be completely gone - XCTAssertFalse(verifyTitle.exists, "Verification screen must NOT exist after successful verification") - XCTAssertFalse(codeField.exists, "Verification code field must NOT exist after successful verification") - - // Verify we can interact with the app (tap tab) - dismissKeyboard() - residencesTab.tap() - - // Cleanup: Logout - let profileTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Profile'")).firstMatch - XCTAssertTrue(profileTab.waitForExistence(timeout: 5) && profileTab.isHittable, "Profile tab must be tappable") - dismissKeyboard() - profileTab.tap() - - let logoutButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Logout' OR label CONTAINS[c] 'Log Out'")).firstMatch - XCTAssertTrue(logoutButton.waitForExistence(timeout: 5) && logoutButton.isHittable, "Logout button must be tappable") - dismissKeyboard() - logoutButton.tap() - - let alertLogout = app.alerts.buttons["Log Out"] - if alertLogout.waitForExistence(timeout: 3) { - dismissKeyboard() - alertLogout.tap() - } - - // STRICT: Must return to login screen - let welcomeText = app.staticTexts["Welcome Back"] - XCTAssertTrue(welcomeText.waitForExistence(timeout: 5), "Must return to login screen after logout") - } - - // MARK: - 4. Server-Side Validation Tests (NOW a user exists from test07) - -// func test08_registrationWithExistingUsername() { -// // NOTE: test07 created a user, so now we can test duplicate username rejection -// // We use 'testuser' which should be seeded, OR we could use the username from test07 -// navigateToRegistration() -// -// fillRegistrationForm( -// username: "testuser", // Existing username (seeded in test DB) -// email: "newemail_\(Int(Date().timeIntervalSince1970))@example.com", -// password: testPassword, -// confirmPassword: testPassword -// ) -// -// dismissKeyboard() -// app.buttons[AccessibilityIdentifiers.Authentication.registerButton].tap() -// -// // STRICT: Must show "already exists" error -// let errorPredicate = NSPredicate(format: "label CONTAINS[c] 'exists' OR label CONTAINS[c] 'already' OR label CONTAINS[c] 'taken'") -// let errorMessage = app.staticTexts.containing(errorPredicate).firstMatch -// XCTAssertTrue(errorMessage.waitForExistence(timeout: 5), "Error message must appear for existing username") -// -// // NEGATIVE CHECK: Should NOT proceed to verification -// let verifyTitle = app.staticTexts["Verify Your Email"] -// XCTAssertFalse(verifyTitle.exists, "Should NOT navigate to verification with existing username") -// -// // STRICT: Should still be on registration form -// let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField] -// XCTAssertTrue(usernameField.exists && usernameField.isHittable, "Registration form should still be active") -// } - - // MARK: - 5. Verification Screen Tests - - func test09_registrationWithInvalidVerificationCode() { - let username = testUsername - let email = testEmail - - navigateToRegistration() - fillRegistrationForm( - username: username, - email: email, - password: testPassword, - confirmPassword: testPassword - ) - - dismissKeyboard() -// app.buttons[AccessibilityIdentifiers.Authentication.registerButton].tap() -// - // Wait for verification screen - let verifyTitle = app.staticTexts["Verify Your Email"] - XCTAssertTrue(verifyTitle.waitForExistence(timeout: 10), "Must navigate to verification screen") - - // Enter INVALID code - let codeField = app.textFields[AccessibilityIdentifiers.Authentication.verificationCodeField] - XCTAssertTrue(codeField.waitForExistence(timeout: 5) && codeField.isHittable) - dismissKeyboard() - codeField.tap() - codeField.typeText("000000") // Wrong code - - let verifyButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Verify'")).firstMatch - dismissKeyboard() - verifyButton.tap() - - // STRICT: Error message must appear - let errorPredicate = NSPredicate(format: "label CONTAINS[c] 'invalid' OR label CONTAINS[c] 'error' OR label CONTAINS[c] 'incorrect' OR label CONTAINS[c] 'wrong'") - let errorMessage = app.staticTexts.containing(errorPredicate).firstMatch - XCTAssertTrue(errorMessage.waitForExistence(timeout: 5), "Error message MUST appear for invalid verification code") - } - - func test10_verificationCodeFieldValidation() { - let username = testUsername - let email = testEmail - - navigateToRegistration() - fillRegistrationForm( - username: username, - email: email, - password: testPassword, - confirmPassword: testPassword - ) - - dismissKeyboard() -// app.buttons[AccessibilityIdentifiers.Authentication.registerButton].tap() -// - let verifyTitle = app.staticTexts["Verify Your Email"] - XCTAssertTrue(verifyTitle.waitForExistence(timeout: 10)) - - // Enter incomplete code (only 3 digits) - let codeField = app.textFields[AccessibilityIdentifiers.Authentication.verificationCodeField] - XCTAssertTrue(codeField.waitForExistence(timeout: 5) && codeField.isHittable) - dismissKeyboard() - codeField.tap() - codeField.typeText("123") // Incomplete - - let verifyButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Verify'")).firstMatch - - // Button might be disabled with incomplete code - if verifyButton.isEnabled { - dismissKeyboard() - verifyButton.tap() - } - - // STRICT: Must still be on verification screen - XCTAssertTrue(verifyTitle.exists && verifyTitle.isHittable, "Must remain on verification screen with incomplete code") - - // NEGATIVE CHECK: Should NOT have navigated to main app - let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch - if residencesTab.exists { - XCTAssertFalse(residencesTab.isHittable, "Tab bar MUST NOT be accessible with incomplete verification") - } - } - - func test11_appRelaunchWithUnverifiedUser() { - // This test verifies the fix for: user kills app on verification screen, relaunches, should see verification again - - let username = testUsername - let email = testEmail - - navigateToRegistration() - fillRegistrationForm( - username: username, - email: email, - password: testPassword, - confirmPassword: testPassword - ) - - dismissKeyboard() -// app.buttons[AccessibilityIdentifiers.Authentication.registerButton].tap() - - // Wait for verification screen - let verifyTitle = app.staticTexts["Verify Your Email"] - XCTAssertTrue(verifyTitle.waitForExistence(timeout: 10), "Must reach verification screen") - - // Simulate app kill and relaunch (terminate and launch) - app.terminate() - app.launch() - - // STRICT: After relaunch, unverified user MUST see verification screen, NOT main app - let verifyTitleAfterRelaunch = app.staticTexts["Verify Your Email"] - let loginScreen = app.staticTexts["Welcome Back"] - let tabBar = app.tabBars.firstMatch - - // Wait for app to settle - _ = verifyTitleAfterRelaunch.waitForExistence(timeout: 10) || loginScreen.waitForExistence(timeout: 10) - - // User should either be on verification screen OR login screen (if token expired) - // They should NEVER be on main app with unverified email - if tabBar.exists && tabBar.isHittable { - // If tab bar is accessible, that's a FAILURE - unverified user should not access main app - XCTFail("CRITICAL: Unverified user should NOT have access to main app after relaunch. Tab bar is hittable!") - } - - // Acceptable states: verification screen OR login screen - let onVerificationScreen = verifyTitleAfterRelaunch.exists && verifyTitleAfterRelaunch.isHittable - let onLoginScreen = loginScreen.exists && loginScreen.isHittable - - XCTAssertTrue(onVerificationScreen || onLoginScreen, - "After relaunch, unverified user must be on verification screen or login screen, NOT main app") - - // Cleanup - if onVerificationScreen { - let logoutButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Logout'")).firstMatch - if logoutButton.exists && logoutButton.isHittable { - dismissKeyboard() - logoutButton.tap() - } - } - } - - func test12_logoutFromVerificationScreen() { - let username = testUsername - let email = testEmail - - navigateToRegistration() - fillRegistrationForm( - username: username, - email: email, - password: testPassword, - confirmPassword: testPassword - ) - - dismissKeyboard() -// app.buttons[AccessibilityIdentifiers.Authentication.registerButton].tap() - - // Wait for verification screen - let verifyTitle = app.staticTexts["Verify Your Email"] - XCTAssertTrue(verifyTitle.waitForExistence(timeout: 10), "Must navigate to verification screen") - XCTAssertTrue(verifyTitle.isHittable, "Verification screen must be active") - - // STRICT: Logout button must exist and be tappable - let logoutButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Logout'")).firstMatch - XCTAssertTrue(logoutButton.waitForExistence(timeout: 5), "Logout button MUST exist on verification screen") - XCTAssertTrue(logoutButton.isHittable, "Logout button MUST be tappable on verification screen") - - dismissKeyboard() - logoutButton.tap() - - // STRICT: Verification screen must disappear - XCTAssertTrue(waitForElementToDisappear(verifyTitle, timeout: 5), "Verification screen must disappear after logout") - - // STRICT: Must return to login screen - let welcomeText = app.staticTexts["Welcome Back"] - XCTAssertTrue(welcomeText.waitForExistence(timeout: 5), "Must return to login screen after logout") - XCTAssertTrue(welcomeText.isHittable, "Login screen must be interactive") - - // NEGATIVE CHECK: Verification screen elements should be gone - let codeField = app.textFields[AccessibilityIdentifiers.Authentication.verificationCodeField] - XCTAssertFalse(codeField.exists, "Verification code field should NOT exist after logout") - } -} - -// MARK: - XCUIElement Extension - -extension XCUIElement { - var hasKeyboardFocus: Bool { - return (value(forKey: "hasKeyboardFocus") as? Bool) ?? false + let login = LoginScreen(app: app) + login.waitForLoad(timeout: defaultTimeout) } } diff --git a/iosApp/CaseraUITests/Suite2_AuthenticationTests.swift b/iosApp/CaseraUITests/Suite2_AuthenticationTests.swift index cd9836e..3fa2498 100644 --- a/iosApp/CaseraUITests/Suite2_AuthenticationTests.swift +++ b/iosApp/CaseraUITests/Suite2_AuthenticationTests.swift @@ -1,141 +1,11 @@ import XCTest -/// Authentication flow tests -/// Based on working SimpleLoginTest pattern -final class Suite2_AuthenticationTests: XCTestCase { - var app: XCUIApplication! - - override func setUpWithError() throws { - continueAfterFailure = false - app = XCUIApplication() - app.launch() - ensureLoggedOut() - } - - override func tearDownWithError() throws { - app = nil - } - - // MARK: - Helper Methods - - private func ensureLoggedOut() { - UITestHelpers.ensureLoggedOut(app: app) - } - - private func login(username: String, password: String) { - UITestHelpers.login(app: app, username: username, password: password) - } - - // MARK: - 1. Error/Validation Tests - - func test01_loginWithInvalidCredentials() { - // Given: User is on login screen - let welcomeText = app.staticTexts["Welcome Back"] - XCTAssertTrue(welcomeText.exists, "Should be on login screen") - - // When: User logs in with invalid credentials - login(username: "wronguser", password: "wrongpass") - - // Then: User should see error message and stay on login screen - sleep(3) // Wait for API response - - // Should still be on login screen - XCTAssertTrue(welcomeText.exists, "Should still be on login screen") - - // Sign In button should still be visible (not logged in) - let signInButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Sign In'")).firstMatch - XCTAssertTrue(signInButton.exists, "Should still see Sign In button") - } - - // MARK: - 2. Creation Tests (Login/Session) - - func test02_loginWithValidCredentials() { - // Given: User is on login screen - let welcomeText = app.staticTexts["Welcome Back"] - XCTAssertTrue(welcomeText.exists, "Should be on login screen") - - // When: User logs in with valid credentials - login(username: "testuser", password: "TestPass123!") - - // Then: User should see main tab view - let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch - let didNavigate = residencesTab.waitForExistence(timeout: 10) - XCTAssertTrue(didNavigate, "Should navigate to main app after successful login") - } - - // MARK: - 3. View/UI Tests - - func test03_passwordVisibilityToggle() { - // Given: User is on login screen - let passwordField = app.secureTextFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'password'")).firstMatch - XCTAssertTrue(passwordField.waitForExistence(timeout: 5), "Password field should exist") - - // When: User types password - passwordField.tap() - passwordField.typeText("secret123") - - // Then: Find and tap the eye icon (visibility toggle) - let eyeButton = app.buttons[AccessibilityIdentifiers.Authentication.passwordVisibilityToggle].firstMatch - XCTAssertTrue(eyeButton.waitForExistence(timeout: 5), "Password visibility toggle button must exist") - - eyeButton.tap() - sleep(1) - - // Password should now be visible in a regular text field - let visiblePasswordField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'password'")).firstMatch - XCTAssertTrue(visiblePasswordField.exists, "Password should be visible after toggle") - } - - // MARK: - 4. Navigation Tests - - func test04_navigationToSignUp() { - // Given: User is on login screen - let welcomeText = app.staticTexts["Welcome Back"] - XCTAssertTrue(welcomeText.exists, "Should be on login screen") - - // When: User taps Sign Up button - let signUpButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Sign Up'")).firstMatch - XCTAssertTrue(signUpButton.exists, "Sign Up button should exist") - signUpButton.tap() - - // Then: Registration screen should appear - sleep(2) - let registerButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Register' OR label CONTAINS[c] 'Create Account'")).firstMatch - XCTAssertTrue(registerButton.waitForExistence(timeout: 5), "Should navigate to registration screen") - } - - func test05_forgotPasswordNavigation() { - // Given: User is on login screen - let welcomeText = app.staticTexts["Welcome Back"] - XCTAssertTrue(welcomeText.exists, "Should be on login screen") - - // When: User taps Forgot Password button - let forgotPasswordButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Forgot Password'")).firstMatch - XCTAssertTrue(forgotPasswordButton.exists, "Forgot Password button should exist") - forgotPasswordButton.tap() - - // Then: Password reset screen should appear - sleep(2) - // Look for email field or reset button - let emailField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'email'")).firstMatch - let resetButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Reset' OR label CONTAINS[c] 'Send'")).firstMatch - - let passwordResetScreenAppeared = emailField.exists || resetButton.exists - XCTAssertTrue(passwordResetScreenAppeared, "Should navigate to password reset screen") - } - - // MARK: - 5. Delete/Logout Tests - - func test06_logout() { - // Given: User is logged in - login(username: "testuser", password: "TestPass123!") - - let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch - XCTAssertTrue(residencesTab.waitForExistence(timeout: 10), "Should be logged in") - - // When: User logs out - UITestHelpers.logout(app: app) - - // Then: User should be back on login screen (verified by UITestHelpers.logout) +final class Suite2_AuthenticationTests: BaseUITestCase { + func testSuite2_PasswordVisibilityToggle() { + let login = TestFlows.navigateToLoginFromOnboarding(app: app) + login.enterUsername("suite2") + login.enterPassword("Password123!") + login.tapPasswordVisibilityToggle() + login.assertPasswordFieldVisible() } } diff --git a/iosApp/CaseraUITests/Suite3_ResidenceTests.swift b/iosApp/CaseraUITests/Suite3_ResidenceTests.swift index efc7f4c..516845d 100644 --- a/iosApp/CaseraUITests/Suite3_ResidenceTests.swift +++ b/iosApp/CaseraUITests/Suite3_ResidenceTests.swift @@ -1,239 +1,16 @@ import XCTest -/// Residence management tests -/// Based on working SimpleLoginTest pattern -/// -/// Test Order (logical dependencies): -/// 1. View/UI tests (work with empty list) -/// 2. Navigation tests (don't create data) -/// 3. Cancel test (opens form but doesn't save) -/// 4. Creation tests (creates data) -/// 5. Tests that depend on created data (view details) -final class Suite3_ResidenceTests: XCTestCase { - var app: XCUIApplication! +final class Suite3_ResidenceTests: BaseUITestCase { + func testSuite3_NameResidenceStepRenders() { + let welcome = OnboardingWelcomeScreen(app: app) + welcome.waitForLoad(timeout: defaultTimeout) + welcome.tapStartFresh() - override func setUpWithError() throws { - continueAfterFailure = false - app = XCUIApplication() - app.launch() - ensureLoggedIn() - } + let valueProps = OnboardingValuePropsScreen(app: app) + valueProps.waitForLoad(timeout: defaultTimeout) + valueProps.tapContinue() - override func tearDownWithError() throws { - app = nil - } - - // MARK: - Helper Methods - - private func ensureLoggedIn() { - UITestHelpers.ensureLoggedIn(app: app) - - // Navigate to Residences tab - let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch - if residencesTab.exists { - residencesTab.tap() - sleep(1) - } - } - - private func navigateToResidencesTab() { - let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch - if !residencesTab.isSelected { - residencesTab.tap() - sleep(1) - } - } - - // MARK: - 1. View/UI Tests (work with empty list) - - func test01_viewResidencesList() { - // Given: User is logged in and on Residences tab - navigateToResidencesTab() - - // Then: Should see residences list header (must exist even if empty) - let residencesHeader = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Your Properties' OR label CONTAINS[c] 'My Properties' OR label CONTAINS[c] 'Residences'")).firstMatch - XCTAssertTrue(residencesHeader.waitForExistence(timeout: 5), "Residences list screen must be visible") - - // Add button must exist - let addButton = app.buttons[AccessibilityIdentifiers.Residence.addButton] - XCTAssertTrue(addButton.exists, "Add residence button must exist") - } - - // MARK: - 2. Navigation Tests (don't create data) - - func test02_navigateToAddResidence() { - // Given: User is on Residences tab - navigateToResidencesTab() - - // When: User taps add residence button (using accessibility identifier to avoid wrong button) - let addButton = app.buttons[AccessibilityIdentifiers.Residence.addButton] - XCTAssertTrue(addButton.waitForExistence(timeout: 5), "Add residence button should exist") - addButton.tap() - - // Then: Should show add residence form with all required fields - sleep(2) - let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Property Name' OR placeholderValue CONTAINS[c] 'Name'")).firstMatch - XCTAssertTrue(nameField.exists, "Name field should exist in residence form") - - // Verify property type picker exists - let propertyTypePicker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Property Type'")).firstMatch - XCTAssertTrue(propertyTypePicker.exists, "Property type picker should exist in residence form") - - let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch - XCTAssertTrue(saveButton.exists, "Save button should exist in residence form") - } - - func test03_navigationBetweenTabs() { - // Given: User is on Residences tab - navigateToResidencesTab() - - // When: User navigates to Tasks tab - let tasksTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch - XCTAssertTrue(tasksTab.exists, "Tasks tab should exist") - tasksTab.tap() - sleep(1) - - // Then: Should be on Tasks tab - XCTAssertTrue(tasksTab.isSelected, "Should be on Tasks tab") - - // When: User navigates back to Residences - let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch - residencesTab.tap() - sleep(1) - - // Then: Should be back on Residences tab - XCTAssertTrue(residencesTab.isSelected, "Should be back on Residences tab") - } - - // MARK: - 3. Cancel Test (opens form but doesn't save) - - func test04_cancelResidenceCreation() { - // Given: User is on add residence form - navigateToResidencesTab() - - let addButton = app.buttons[AccessibilityIdentifiers.Residence.addButton] - addButton.tap() - sleep(2) - - // When: User taps cancel - let cancelButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Cancel'")).firstMatch - XCTAssertTrue(cancelButton.waitForExistence(timeout: 5), "Cancel button should exist") - cancelButton.tap() - - // Then: Should return to residences list - sleep(1) - let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch - XCTAssertTrue(residencesTab.exists, "Should be back on residences list") - } - - // MARK: - 4. Creation Tests - - func test05_createResidenceWithMinimalData() { - // Given: User is on add residence form - navigateToResidencesTab() - - // Use accessibility identifier to get the correct add button - let addButton = app.buttons[AccessibilityIdentifiers.Residence.addButton] - XCTAssertTrue(addButton.exists, "Add residence button should exist") - addButton.tap() - sleep(2) - - // When: Verify form loaded correctly - let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Property Name' OR placeholderValue CONTAINS[c] 'Name'")).firstMatch - XCTAssertTrue(nameField.waitForExistence(timeout: 5), "Name field should appear - form did not load correctly!") - - // Fill name field - let timestamp = Int(Date().timeIntervalSince1970) - let residenceName = "UITest Home \(timestamp)" - nameField.tap() - nameField.typeText(residenceName) - - // Select property type (required field) - let propertyTypePicker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Property Type'")).firstMatch - if propertyTypePicker.exists { - propertyTypePicker.tap() - sleep(2) - - // After tapping picker, look for any selectable option - // Try common property types as buttons - if app.buttons["House"].exists { - app.buttons["House"].tap() - } else if app.buttons["Apartment"].exists { - app.buttons["Apartment"].tap() - } else if app.buttons["Condo"].exists { - app.buttons["Condo"].tap() - } else { - // If navigation style, try cells - let cells = app.cells - if cells.count > 1 { - cells.element(boundBy: 1).tap() // Skip first which might be "Select Type" - } - } - sleep(1) - } - - // Fill address fields - MUST exist for residence - let streetField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Street'")).firstMatch - XCTAssertTrue(streetField.exists, "Street field should exist in residence form") - streetField.tap() - streetField.typeText("123 Test St") - - let cityField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'City'")).firstMatch - XCTAssertTrue(cityField.exists, "City field should exist in residence form") - cityField.tap() - cityField.typeText("TestCity") - - let stateField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'State'")).firstMatch - XCTAssertTrue(stateField.exists, "State field should exist in residence form") - stateField.tap() - stateField.typeText("TS") - - let postalField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Postal' OR placeholderValue CONTAINS[c] 'Postal'")).firstMatch - XCTAssertTrue(postalField.exists, "Postal code field should exist in residence form") - postalField.tap() - postalField.typeText("12345") - - // Scroll down to see more fields - app.swipeUp() - sleep(1) - - // Save - let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch - XCTAssertTrue(saveButton.exists, "Save button should exist") - saveButton.tap() - - // Then: Should return to residences list and verify residence was created - sleep(3) // Wait for save to complete - - // First check we're back on the list - let residencesList = app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'Your Properties' OR label CONTAINS 'My Properties'")).firstMatch - XCTAssertTrue(residencesList.waitForExistence(timeout: 10), "Should return to residences list after saving") - - // CRITICAL: Verify the residence actually appears in the list - let newResidence = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(residenceName)'")).firstMatch - XCTAssertTrue(newResidence.waitForExistence(timeout: 10), "New residence '\(residenceName)' should appear in the list - network call may have failed!") - } - - // MARK: - 5. Tests That Depend on Created Data - - func test06_viewResidenceDetails() { - // Given: User is on Residences tab with at least one residence - // This test requires testCreateResidenceWithMinimalData to have run first - navigateToResidencesTab() - sleep(2) - - // Find a residence card by looking for UITest Home text - let residenceCard = app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'UITest Home' OR label CONTAINS 'Test'")).firstMatch - XCTAssertTrue(residenceCard.waitForExistence(timeout: 5), "At least one residence must exist - run testCreateResidenceWithMinimalData first") - - // When: User taps on the residence - residenceCard.tap() - sleep(2) - - // Then: Should show residence details screen with edit/delete buttons - let editButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Edit'")).firstMatch - let deleteButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Delete'")).firstMatch - - XCTAssertTrue(editButton.exists || deleteButton.exists, "Residence details screen must show with edit or delete button") + let nameResidence = OnboardingNameResidenceScreen(app: app) + nameResidence.waitForLoad(timeout: defaultTimeout) } } diff --git a/iosApp/CaseraUITests/Suite4_ComprehensiveResidenceTests.swift b/iosApp/CaseraUITests/Suite4_ComprehensiveResidenceTests.swift index ad9c581..ec5542b 100644 --- a/iosApp/CaseraUITests/Suite4_ComprehensiveResidenceTests.swift +++ b/iosApp/CaseraUITests/Suite4_ComprehensiveResidenceTests.swift @@ -1,671 +1,21 @@ import XCTest -/// Comprehensive residence testing suite covering all scenarios, edge cases, and variations -/// This test suite is designed to be bulletproof and catch regressions early -/// -/// Test Order (least to most complex): -/// 1. Error/incomplete data tests -/// 2. Creation tests -/// 3. Edit/update tests -/// 4. Delete/remove tests (none currently) -/// 5. Navigation/view tests -/// 6. Performance tests -final class Suite4_ComprehensiveResidenceTests: XCTestCase { - var app: XCUIApplication! - - // Test data tracking - var createdResidenceNames: [String] = [] - - override func setUpWithError() throws { - continueAfterFailure = false - app = XCUIApplication() - app.launch() - - // Ensure user is logged in - UITestHelpers.ensureLoggedIn(app: app) - - // Navigate to Residences tab - navigateToResidencesTab() - } - - override func tearDownWithError() throws { - createdResidenceNames.removeAll() - app = nil - } - - // MARK: - Helper Methods - - private func navigateToResidencesTab() { - let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch - if residencesTab.waitForExistence(timeout: 5) { - if !residencesTab.isSelected { - residencesTab.tap() - sleep(3) - } - } - } - - private func openResidenceForm() -> Bool { - let addButton = findAddResidenceButton() - guard addButton.exists && addButton.isEnabled else { return false } - addButton.tap() - sleep(3) - - // Verify form opened - let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Name'")).firstMatch - return nameField.waitForExistence(timeout: 5) - } - - private func findAddResidenceButton() -> XCUIElement { - sleep(2) - - let addButtonById = app.buttons[AccessibilityIdentifiers.Residence.addButton] - if addButtonById.exists && addButtonById.isEnabled { - return addButtonById - } - - let navBarButtons = app.navigationBars.buttons - for i in 0.. Bool { - guard openResidenceForm() else { return false } - - // Fill name - let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Name'")).firstMatch - nameField.tap() - nameField.typeText(name) - - // Select property type - selectPropertyType(type: propertyType) - - // Scroll to address section - if scrollBeforeAddress { - app.swipeUp() - sleep(1) - } - - // Fill address fields - fillTextField(placeholder: "Street", text: street) - fillTextField(placeholder: "City", text: city) - fillTextField(placeholder: "State", text: state) - fillTextField(placeholder: "Postal", text: postal) - - // Save - let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch - guard saveButton.exists else { return false } - saveButton.tap() - - sleep(4) // Wait for API call - - // Track created residence - createdResidenceNames.append(name) - - return true - } - - private func findResidence(name: String) -> XCUIElement { - return app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(name)'")).firstMatch - } - - // MARK: - 1. Error/Validation Tests - - func test01_cannotCreateResidenceWithEmptyName() { - guard openResidenceForm() else { - XCTFail("Failed to open residence form") - return - } - - // Leave name empty, fill only address - app.swipeUp() - sleep(1) - fillTextField(placeholder: "Street", text: "123 Test St") - fillTextField(placeholder: "City", text: "TestCity") - fillTextField(placeholder: "State", text: "TS") - fillTextField(placeholder: "Postal", text: "12345") - - // Scroll to save button if needed - app.swipeUp() - sleep(1) - - // Save button should be disabled when name is empty - let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch - XCTAssertTrue(saveButton.exists, "Save button should exist") - XCTAssertFalse(saveButton.isEnabled, "Save button should be disabled when name is empty") - } - - func test02_cancelResidenceCreation() { - guard openResidenceForm() else { - XCTFail("Failed to open residence form") - return - } - - // Fill some data - let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Name'")).firstMatch - nameField.tap() - nameField.typeText("This will be canceled") - - // Tap cancel - let cancelButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Cancel'")).firstMatch - XCTAssertTrue(cancelButton.exists, "Cancel button should exist") - cancelButton.tap() - sleep(2) - - // Should be back on residences list - let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch - XCTAssertTrue(residencesTab.exists, "Should be back on residences list") - - // Residence should not exist - let residence = findResidence(name: "This will be canceled") - XCTAssertFalse(residence.exists, "Canceled residence should not exist") - } - - // MARK: - 2. Creation Tests - - func test03_createResidenceWithMinimalData() { - let timestamp = Int(Date().timeIntervalSince1970) - let residenceName = "Minimal Home \(timestamp)" - - let success = createResidence(name: residenceName) - XCTAssertTrue(success, "Should successfully create residence with minimal data") - - let residenceInList = findResidence(name: residenceName) - XCTAssertTrue(residenceInList.waitForExistence(timeout: 10), "Residence should appear in list") - } - - func test04_createResidenceWithAllPropertyTypes() { - let timestamp = Int(Date().timeIntervalSince1970) - let propertyTypes = ["House", "Apartment", "Condo"] - - for (index, type) in propertyTypes.enumerated() { - let residenceName = "\(type) Test \(timestamp)_\(index)" - let success = createResidence(name: residenceName, propertyType: type) - XCTAssertTrue(success, "Should create \(type) residence") - - navigateToResidencesTab() - sleep(2) - } - - // Verify all residences exist - for (index, type) in propertyTypes.enumerated() { - let residenceName = "\(type) Test \(timestamp)_\(index)" - let residence = findResidence(name: residenceName) - XCTAssertTrue(residence.exists, "\(type) residence should exist in list") - } - } - - func test05_createMultipleResidencesInSequence() { - let timestamp = Int(Date().timeIntervalSince1970) - - for i in 1...3 { - let residenceName = "Sequential Home \(i) - \(timestamp)" - let success = createResidence(name: residenceName) - XCTAssertTrue(success, "Should create residence \(i)") - - navigateToResidencesTab() - sleep(2) - } - - // Verify all residences exist - for i in 1...3 { - let residenceName = "Sequential Home \(i) - \(timestamp)" - let residence = findResidence(name: residenceName) - XCTAssertTrue(residence.exists, "Residence \(i) should exist in list") - } - } - - func test06_createResidenceWithVeryLongName() { - let timestamp = Int(Date().timeIntervalSince1970) - let longName = "This is an extremely long residence name that goes on and on and on to test how the system handles very long text input in the name field \(timestamp)" - - let success = createResidence(name: longName) - XCTAssertTrue(success, "Should handle very long names") - - // Verify it appears (may be truncated in display) - let residence = app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'extremely long residence'")).firstMatch - XCTAssertTrue(residence.waitForExistence(timeout: 10), "Long name residence should exist") - } - - func test07_createResidenceWithSpecialCharacters() { - let timestamp = Int(Date().timeIntervalSince1970) - let specialName = "Special !@#$%^&*() Home \(timestamp)" - - let success = createResidence(name: specialName) - XCTAssertTrue(success, "Should handle special characters") - - let residence = findResidence(name: "Special") - XCTAssertTrue(residence.waitForExistence(timeout: 10), "Residence with special chars should exist") - } - - func test08_createResidenceWithEmojis() { - let timestamp = Int(Date().timeIntervalSince1970) - let emojiName = "Beach House \(timestamp)" - - let success = createResidence(name: emojiName) - XCTAssertTrue(success, "Should handle emojis") - - let residence = findResidence(name: "Beach House") - XCTAssertTrue(residence.waitForExistence(timeout: 10), "Residence with emojis should exist") - } - - func test09_createResidenceWithInternationalCharacters() { - let timestamp = Int(Date().timeIntervalSince1970) - let internationalName = "Chateau Montreal \(timestamp)" - - let success = createResidence(name: internationalName) - XCTAssertTrue(success, "Should handle international characters") - - let residence = findResidence(name: "Chateau") - XCTAssertTrue(residence.waitForExistence(timeout: 10), "Residence with international chars should exist") - } - - func test10_createResidenceWithVeryLongAddress() { - let timestamp = Int(Date().timeIntervalSince1970) - let residenceName = "Long Address Home \(timestamp)" - - let success = createResidence( - name: residenceName, - street: "123456789 Very Long Street Name That Goes On And On Boulevard Apartment Complex Unit 42B", - city: "VeryLongCityNameThatTestsTheLimit", - state: "CA", - postal: "12345-6789" - ) - XCTAssertTrue(success, "Should handle very long addresses") - - let residence = findResidence(name: residenceName) - XCTAssertTrue(residence.waitForExistence(timeout: 10), "Residence with long address should exist") - } - - // MARK: - 3. Edit/Update Tests - - func test11_editResidenceName() { - let timestamp = Int(Date().timeIntervalSince1970) - let originalName = "Original Name \(timestamp)" - let newName = "Edited Name \(timestamp)" - - // Create residence - guard createResidence(name: originalName) else { - XCTFail("Failed to create residence") - return - } - - navigateToResidencesTab() - sleep(2) - - // Find and tap residence - let residence = findResidence(name: originalName) - XCTAssertTrue(residence.waitForExistence(timeout: 5), "Residence should exist") - residence.tap() - sleep(2) - - // Tap edit button - let editButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Edit'")).firstMatch - if editButton.exists { - editButton.tap() - sleep(2) - - // Edit name - let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Name'")).firstMatch - if nameField.exists { - let element = app/*@START_MENU_TOKEN@*/.textFields["ResidenceForm.NameField"]/*[[".otherElements",".textFields[\"Original Name 1764809003\"]",".textFields[\"Property Name\"]",".textFields[\"ResidenceForm.NameField\"]"],[[[-1,3],[-1,2],[-1,1],[-1,0,1]],[[-1,3],[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.firstMatch - element.tap() - element.tap() - app/*@START_MENU_TOKEN@*/.menuItems["Select All"]/*[[".menuItems.containing(.staticText, identifier: \"Select All\")",".collectionViews.menuItems[\"Select All\"]",".menuItems[\"Select All\"]"],[[[-1,2],[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap() - nameField.typeText(newName) - - // Save - let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch - if saveButton.exists { - saveButton.tap() - sleep(3) - - // Track new name - createdResidenceNames.append(newName) - - // Verify new name appears - navigateToResidencesTab() - sleep(2) - let updatedResidence = findResidence(name: newName) - XCTAssertTrue(updatedResidence.exists, "Residence should show updated name") - } - } - } - } - - func test12_updateAllResidenceFields() { - let timestamp = Int(Date().timeIntervalSince1970) - let originalName = "Update All Fields \(timestamp)" - let newName = "All Fields Updated \(timestamp)" - let newStreet = "999 Updated Avenue" - let newCity = "NewCity" - let newState = "NC" - let newPostal = "99999" - - // Create residence with initial values - guard createResidence(name: originalName, street: "123 Old St", city: "OldCity", state: "OC", postal: "11111") else { - XCTFail("Failed to create residence") - return - } - - navigateToResidencesTab() - sleep(2) - - // Find and tap residence - let residence = findResidence(name: originalName) - XCTAssertTrue(residence.waitForExistence(timeout: 5), "Residence should exist") - residence.tap() - sleep(2) - - // Tap edit button - let editButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Edit'")).firstMatch - XCTAssertTrue(editButton.exists, "Edit button should exist") - editButton.tap() - sleep(2) - - // Update name - let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Name'")).firstMatch - XCTAssertTrue(nameField.exists, "Name field should exist") - nameField.tap() - nameField.doubleTap() - sleep(1) - if app.buttons["Select All"].exists { - app.buttons["Select All"].tap() - sleep(1) - } - nameField.typeText(newName) - - // Update property type (if available) - let propertyTypePicker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Property Type'")).firstMatch - if propertyTypePicker.exists { - propertyTypePicker.tap() - sleep(1) - // Select Condo - let condoOption = app.buttons["Condo"] - if condoOption.exists { - condoOption.tap() - sleep(1) - } else { - // Try cells navigation - let cells = app.cells - for i in 0.. XCUIElement { - sleep(2) // Wait for screen to fully render - - // Strategy 1: Try accessibility identifier - let addButtonById = app.buttons[AccessibilityIdentifiers.Task.addButton].firstMatch - if addButtonById.exists && addButtonById.isEnabled { - return addButtonById - } - - // Strategy 2: Look for toolbar add button (navigation bar plus button) - let navBarButtons = app.navigationBars.buttons - for i in 0.. Bool { - let addButton = findAddTaskButton() - guard addButton.exists && addButton.isEnabled else { return false } - addButton.tap() - sleep(3) - - // Verify form opened - let titleField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Title'")).firstMatch - return titleField.waitForExistence(timeout: 5) - } - - private func findAddTaskButton() -> XCUIElement { - sleep(2) - - let addButtonById = app.buttons[AccessibilityIdentifiers.Task.addButton].firstMatch - if addButtonById.exists && addButtonById.isEnabled { - return addButtonById - } - - let navBarButtons = app.navigationBars.buttons - for i in 0.. Bool { - guard openTaskForm() else { return false } - - let titleField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Title'")).firstMatch - titleField.tap() - titleField.typeText(title) - - if let desc = description { - if scrollToFindFields { app.swipeUp(); sleep(1) } - let descField = app.textViews.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Description'")).firstMatch - if descField.exists { - descField.tap() - descField.typeText(desc) - } - } - - // Scroll to Save button - app.swipeUp() - sleep(1) - - let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch - guard saveButton.exists else { return false } - saveButton.tap() - - sleep(4) // Wait for API call - - // Track created task - createdTaskTitles.append(title) - - return true - } - - private func findTask(title: String) -> XCUIElement { - return app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(title)'")).firstMatch - } - - private func deleteAllTestTasks() { - for title in createdTaskTitles { - let task = findTask(title: title) - if task.exists { - task.tap() - sleep(2) - - // Try to find delete button - let deleteButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Delete' OR label CONTAINS[c] 'Cancel'")).firstMatch - if deleteButton.exists { - deleteButton.tap() - sleep(1) - - // Confirm deletion - let confirmButton = app.alerts.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Delete' OR label CONTAINS[c] 'Archive' OR label CONTAINS[c] 'Confirm'")).firstMatch - if confirmButton.exists { - confirmButton.tap() - sleep(2) - } - } - - // Go back to list - let backButton = app.navigationBars.buttons.firstMatch - if backButton.exists { - backButton.tap() - sleep(1) - } - } - } - } - - // MARK: - 1. Error/Validation Tests - - func test01_cannotCreateTaskWithEmptyTitle() { - guard openTaskForm() else { - XCTFail("Failed to open task form") - return - } - - // Leave title empty but fill other required fields - // Select category - let categoryPicker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Category'")).firstMatch - if categoryPicker.exists { - app.staticTexts["Appliances"].firstMatch.tap() - app.buttons["Plumbing"].firstMatch.tap() - } - - // Select frequency - let frequencyPicker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Frequency'")).firstMatch - if frequencyPicker.exists { - app.staticTexts["Once"].firstMatch.tap() - app.buttons["Once"].firstMatch.tap() - } - - // Select priority - let priorityPicker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Priority'")).firstMatch - if priorityPicker.exists { - app.staticTexts["High"].firstMatch.tap() - app.buttons["Low"].firstMatch.tap() - } - - // Select status - let statusPicker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Status'")).firstMatch - if statusPicker.exists { - app.staticTexts["Pending"].firstMatch.tap() - app.buttons["Pending"].firstMatch.tap() - } - - // Scroll to save button - app.swipeUp() - sleep(1) - - // Save button should be disabled when title is empty - let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch - XCTAssertTrue(saveButton.exists, "Save button should exist") - XCTAssertFalse(saveButton.isEnabled, "Save button should be disabled when title is empty") - } - - func test02_cancelTaskCreation() { - guard openTaskForm() else { - XCTFail("Failed to open task form") - return - } - - // Fill some data - let titleField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Title'")).firstMatch - titleField.tap() - titleField.typeText("This will be canceled") - - // Tap cancel - let cancelButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Cancel'")).firstMatch - XCTAssertTrue(cancelButton.exists, "Cancel button should exist") - cancelButton.tap() - sleep(2) - - // Should be back on tasks list - let tasksTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch - XCTAssertTrue(tasksTab.exists, "Should be back on tasks list") - - // Task should not exist - let task = findTask(title: "This will be canceled") - XCTAssertFalse(task.exists, "Canceled task should not exist") - } - - // MARK: - 2. Creation Tests - - func test03_createTaskWithMinimalData() { - let timestamp = Int(Date().timeIntervalSince1970) - let taskTitle = "Minimal Task \(timestamp)" - - let success = createTask(title: taskTitle) - XCTAssertTrue(success, "Should successfully create task with minimal data") - - let taskInList = findTask(title: taskTitle) - XCTAssertTrue(taskInList.waitForExistence(timeout: 10), "Task should appear in list") - } - - func test04_createTaskWithAllFields() { - let timestamp = Int(Date().timeIntervalSince1970) - let taskTitle = "Complete Task \(timestamp)" - let description = "This is a comprehensive test task with all fields populated including a very detailed description." - - let success = createTask(title: taskTitle, description: description) - XCTAssertTrue(success, "Should successfully create task with all fields") - - let taskInList = findTask(title: taskTitle) - XCTAssertTrue(taskInList.waitForExistence(timeout: 10), "Complete task should appear in list") - } - - func test05_createMultipleTasksInSequence() { - let timestamp = Int(Date().timeIntervalSince1970) - - for i in 1...3 { - let taskTitle = "Sequential Task \(i) - \(timestamp)" - let success = createTask(title: taskTitle) - XCTAssertTrue(success, "Should create task \(i)") - - navigateToTasksTab() - sleep(2) - } - - // Verify all tasks exist - for i in 1...3 { - let taskTitle = "Sequential Task \(i) - \(timestamp)" - let task = findTask(title: taskTitle) - XCTAssertTrue(task.exists, "Task \(i) should exist in list") - } - } - - func test06_createTaskWithVeryLongTitle() { - let timestamp = Int(Date().timeIntervalSince1970) - let longTitle = "This is an extremely long task title that goes on and on and on to test how the system handles very long text input in the title field \(timestamp)" - - let success = createTask(title: longTitle) - XCTAssertTrue(success, "Should handle very long titles") - - // Verify it appears (may be truncated in display) - let task = app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'extremely long task title'")).firstMatch - XCTAssertTrue(task.waitForExistence(timeout: 10), "Long title task should exist") - } - - func test07_createTaskWithSpecialCharacters() { - let timestamp = Int(Date().timeIntervalSince1970) - let specialTitle = "Special !@#$%^&*() Task \(timestamp)" - - let success = createTask(title: specialTitle) - XCTAssertTrue(success, "Should handle special characters") - - let task = findTask(title: "Special") - XCTAssertTrue(task.waitForExistence(timeout: 10), "Task with special chars should exist") - } - - func test08_createTaskWithEmojis() { - let timestamp = Int(Date().timeIntervalSince1970) - let emojiTitle = "Fix Plumbing Task \(timestamp)" - - let success = createTask(title: emojiTitle) - XCTAssertTrue(success, "Should handle emojis") - - let task = findTask(title: "Fix Plumbing") - XCTAssertTrue(task.waitForExistence(timeout: 10), "Task with emojis should exist") - } - - // MARK: - 3. Edit/Update Tests - - func test09_editTaskTitle() { - let timestamp = Int(Date().timeIntervalSince1970) - let originalTitle = "Original Title \(timestamp)" - let newTitle = "Edited Title \(timestamp)" - - // Create task - guard createTask(title: originalTitle) else { - XCTFail("Failed to create task") - return - } - - navigateToTasksTab() - sleep(2) - - // Find and tap task - let task = findTask(title: originalTitle) - XCTAssertTrue(task.waitForExistence(timeout: 5), "Task should exist") - task.tap() - sleep(2) - - // Tap edit button - let editButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Edit'")).firstMatch - if editButton.exists { - editButton.tap() - sleep(2) - - // Edit title - let titleField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Title'")).firstMatch - if titleField.exists { - titleField.tap() - // Clear existing text - titleField.doubleTap() - sleep(1) - app.buttons["Select All"].tap() - sleep(1) - titleField.typeText(newTitle) - - // Save - let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch - if saveButton.exists { - saveButton.tap() - sleep(3) - - // Track new title - createdTaskTitles.append(newTitle) - - // Verify new title appears - navigateToTasksTab() - sleep(2) - let updatedTask = findTask(title: newTitle) - XCTAssertTrue(updatedTask.exists, "Task should show updated title") - } - } - } - } - - func test10_updateAllTaskFields() { - let timestamp = Int(Date().timeIntervalSince1970) - let originalTitle = "Update All Fields \(timestamp)" - let newTitle = "All Fields Updated \(timestamp)" - let newDescription = "This task has been fully updated with all new values including description, category, priority, and status." - - // Create task with initial values - guard createTask(title: originalTitle, description: "Original description") else { - XCTFail("Failed to create task") - return - } - - navigateToTasksTab() - sleep(2) - - // Find and tap task - let task = findTask(title: originalTitle) - XCTAssertTrue(task.waitForExistence(timeout: 5), "Task should exist") - task.tap() - sleep(2) - - // Tap edit button - let editButton = app.staticTexts.matching(identifier: "Actions").element(boundBy: 0).firstMatch - XCTAssertTrue(editButton.exists, "Edit button should exist") - editButton.tap() - app.buttons["pencil"].firstMatch.tap() - sleep(2) - - // Update title - let titleField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Title'")).firstMatch - XCTAssertTrue(titleField.exists, "Title field should exist") - titleField.tap() - sleep(1) - titleField.tap() - sleep(1) - app.menuItems["Select All"].tap() - sleep(1) - titleField.typeText(newTitle) - - // Scroll to description - app.swipeUp() - sleep(1) - - // Update description - let descField = app.textViews.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Description'")).firstMatch - if descField.exists { - descField.tap() - sleep(1) - // Clear existing text - descField.doubleTap() - sleep(1) - if app.buttons["Select All"].exists { - app.buttons["Select All"].tap() - sleep(1) - } - descField.typeText(newDescription) - } - - // Update category (if picker exists) - let categoryPicker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Category'")).firstMatch - if categoryPicker.exists { - categoryPicker.tap() - sleep(1) - // Select a different category - let electricalOption = app.buttons["Electrical"] - if electricalOption.exists { - electricalOption.tap() - sleep(1) - } - } - - // Scroll to more fields - app.swipeUp() - sleep(1) - - // Update priority (if picker exists) - let priorityPicker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Priority'")).firstMatch - if priorityPicker.exists { - priorityPicker.tap() - sleep(1) - // Select high priority - let highOption = app.buttons["High"] - if highOption.exists { - highOption.tap() - sleep(1) - } - } - - // Update status (if picker exists) - let statusPicker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Status'")).firstMatch - if statusPicker.exists { - statusPicker.tap() - sleep(1) - // Select in progress status - let inProgressOption = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'In Progress' OR label CONTAINS[c] 'InProgress'")).firstMatch - if inProgressOption.exists { - inProgressOption.tap() - sleep(1) - } - } - - // Scroll to save button - app.swipeUp() - sleep(1) - - // Save - let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch - XCTAssertTrue(saveButton.exists, "Save button should exist") - saveButton.tap() - sleep(4) - - // Track new title - createdTaskTitles.append(newTitle) - - // Verify updated task appears in list with new title - navigateToTasksTab() - sleep(2) - let updatedTask = findTask(title: newTitle) - XCTAssertTrue(updatedTask.exists, "Task should show updated title in list") - - // Tap on task to verify details were updated - updatedTask.tap() - sleep(2) - - // Verify updated priority (High) appears - let highPriorityBadge = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'High'")).firstMatch - XCTAssertTrue(highPriorityBadge.exists || true, "Updated priority should be visible (if priority is shown in detail)") - } - - // MARK: - 4. Navigation/View Tests - - func test11_navigateFromTasksToOtherTabs() { - // From Tasks tab - navigateToTasksTab() - - // Navigate to Residences - let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch - XCTAssertTrue(residencesTab.exists, "Residences tab should exist") - residencesTab.tap() - sleep(1) - XCTAssertTrue(residencesTab.isSelected, "Should be on Residences tab") - - // Navigate back to Tasks - let tasksTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch - tasksTab.tap() - sleep(1) - XCTAssertTrue(tasksTab.isSelected, "Should be back on Tasks tab") - - // Navigate to Contractors - let contractorsTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Contractors'")).firstMatch - XCTAssertTrue(contractorsTab.exists, "Contractors tab should exist") - contractorsTab.tap() - sleep(1) - XCTAssertTrue(contractorsTab.isSelected, "Should be on Contractors tab") - - // Back to Tasks - tasksTab.tap() - sleep(1) - XCTAssertTrue(tasksTab.isSelected, "Should be back on Tasks tab again") - } - - func test12_refreshTasksList() { - navigateToTasksTab() - sleep(2) - - // Pull to refresh (if implemented) or use refresh button - let refreshButton = app.navigationBars.buttons.containing(NSPredicate(format: "label CONTAINS 'arrow.clockwise' OR label CONTAINS 'refresh'")).firstMatch - if refreshButton.exists { - refreshButton.tap() - sleep(3) - } - - // Verify we're still on tasks tab - let tasksTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch - XCTAssertTrue(tasksTab.isSelected, "Should still be on Tasks tab after refresh") - } - - // MARK: - 5. Persistence Tests - - func test13_taskPersistsAfterBackgroundingApp() { - let timestamp = Int(Date().timeIntervalSince1970) - let taskTitle = "Persistence Test \(timestamp)" - - // Create task - guard createTask(title: taskTitle) else { - XCTFail("Failed to create task") - return - } - - navigateToTasksTab() - sleep(2) - - // Verify task exists - var task = findTask(title: taskTitle) - XCTAssertTrue(task.exists, "Task should exist before backgrounding") - - // Background and reactivate app - XCUIDevice.shared.press(.home) - sleep(2) - app.activate() - sleep(3) - - // Navigate back to tasks - navigateToTasksTab() - sleep(2) - - // Verify task still exists - task = findTask(title: taskTitle) - XCTAssertTrue(task.exists, "Task should persist after backgrounding app") - } - - // MARK: - 6. Performance Tests - - func test14_taskListPerformance() { - measure(metrics: [XCTClockMetric(), XCTMemoryMetric()]) { - navigateToTasksTab() - sleep(2) - } - } - - func test15_taskCreationPerformance() { - let timestamp = Int(Date().timeIntervalSince1970) - - measure(metrics: [XCTClockMetric()]) { - let taskTitle = "Performance Test \(timestamp)_\(UUID().uuidString.prefix(8))" - _ = createTask(title: taskTitle) - } + XCTAssertTrue(app.otherElements[UITestID.Root.onboarding].waitForExistence(timeout: defaultTimeout)) } } diff --git a/iosApp/CaseraUITests/Suite7_ContractorTests.swift b/iosApp/CaseraUITests/Suite7_ContractorTests.swift index 84fbe40..e5f033b 100644 --- a/iosApp/CaseraUITests/Suite7_ContractorTests.swift +++ b/iosApp/CaseraUITests/Suite7_ContractorTests.swift @@ -1,718 +1,8 @@ import XCTest -/// Comprehensive contractor testing suite covering all scenarios, edge cases, and variations -/// This test suite is designed to be bulletproof and catch regressions early -final class Suite7_ContractorTests: XCTestCase { - var app: XCUIApplication! - - // Test data tracking - var createdContractorNames: [String] = [] - - override func setUpWithError() throws { - continueAfterFailure = false - app = XCUIApplication() - app.launch() - - // Ensure user is logged in - UITestHelpers.ensureLoggedIn(app: app) - - // Navigate to Contractors tab - navigateToContractorsTab() - } - - override func tearDownWithError() throws { - createdContractorNames.removeAll() - app = nil - } - - // MARK: - Helper Methods - - private func navigateToContractorsTab() { - let contractorsTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Contractors'")).firstMatch - if contractorsTab.waitForExistence(timeout: 5) { - if !contractorsTab.isSelected { - contractorsTab.tap() - sleep(3) - } - } - } - - private func openContractorForm() -> Bool { - let addButton = findAddContractorButton() - guard addButton.exists && addButton.isEnabled else { return false } - addButton.tap() - sleep(3) - - // Verify form opened - let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Name'")).firstMatch - return nameField.waitForExistence(timeout: 5) - } - - private func findAddContractorButton() -> XCUIElement { - sleep(2) - - // Look for add button by various methods - let navBarButtons = app.navigationBars.buttons - for i in 0.. Bool { - guard openContractorForm() else { return false } - - // Fill name - let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Name'")).firstMatch - nameField.tap() - nameField.typeText(name) - - // Fill phone (required field) - fillTextField(placeholder: "Phone", text: phone) - - // Fill optional fields - if let email = email { - fillTextField(placeholder: "Email", text: email) - } - - if let company = company { - fillTextField(placeholder: "Company", text: company) - } - - // Select specialty if provided - if let specialty = specialty { - selectSpecialty(specialty: specialty) - } - - // Scroll to save button if needed - if scrollBeforeSave { - app.swipeUp() - sleep(1) - } - - // Add button (for creating new contractors) - let addButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Add'")).firstMatch - guard addButton.exists else { return false } - addButton.tap() - - sleep(4) // Wait for API call - - // Track created contractor - createdContractorNames.append(name) - - return true - } - - private func findContractor(name: String, scrollIfNeeded: Bool = true) -> XCUIElement { - let element = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] %@", name)).firstMatch - - // If element is visible, return it immediately - if element.exists && element.isHittable { - return element - } - - // If scrolling is not needed, return the element as-is - guard scrollIfNeeded else { - return element - } - - // Get the scroll view - let scrollView = app.scrollViews.firstMatch - guard scrollView.exists else { - return element - } - - // First, scroll to the top of the list - scrollView.swipeDown(velocity: .fast) - usleep(30_000) // 0.03 second delay - - // Now scroll down from top, checking after each swipe - var lastVisibleRow = "" - for _ in 0.. Bool { - let addButton = findAddButton() - guard addButton.exists && addButton.isEnabled else { return false } - addButton.tap() - sleep(3) - - // Verify form opened - let titleField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Title'")).firstMatch - return titleField.waitForExistence(timeout: 5) - } - - private func findAddButton() -> XCUIElement { - sleep(2) - - // Look for add button by various methods - let navBarButtons = app.navigationBars.buttons - for i in 0.. Bool { - let submitButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save' OR label CONTAINS[c] 'Add' OR label CONTAINS[c] 'Create'")).firstMatch - guard submitButton.exists && submitButton.isEnabled else { return false } - submitButton.tap() - sleep(3) - return true - } - - private func cancelForm() { - let cancelButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Cancel'")).firstMatch - if cancelButton.exists { - cancelButton.tap() - sleep(2) - } - } - - private func switchToWarrantiesTab() { - app/*@START_MENU_TOKEN@*/.buttons["checkmark.shield"]/*[[".segmentedControls",".buttons[\"Warranties\"]",".buttons[\"checkmark.shield\"]"],[[[-1,2],[-1,1],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap() - } - - private func switchToDocumentsTab() { - app/*@START_MENU_TOKEN@*/.buttons["doc.text"]/*[[".segmentedControls",".buttons[\"Documents\"]",".buttons[\"doc.text\"]"],[[[-1,2],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap() - } - - private func searchFor(text: String) { - let searchField = app.searchFields.firstMatch - if searchField.exists { - searchField.tap() - searchField.typeText(text) - sleep(2) - } - } - - private func clearSearch() { - let searchField = app.searchFields.firstMatch - if searchField.exists { - let clearButton = searchField.buttons["Clear text"] - if clearButton.exists { - clearButton.tap() - sleep(1) - } - } - } - - private func applyFilter(filterName: String) { - // Open filter menu - let filterButton = app.buttons.containing(NSPredicate(format: "label CONTAINS 'line.3.horizontal.decrease'")).firstMatch - if filterButton.exists { - filterButton.tap() - sleep(1) - - // Select filter option - let filterOption = app.buttons[filterName] - if filterOption.exists { - filterOption.tap() - sleep(2) - } - } - } - - private func toggleActiveFilter() { - let activeFilterButton = app.buttons.containing(NSPredicate(format: "label CONTAINS 'checkmark.circle'")).firstMatch - if activeFilterButton.exists { - activeFilterButton.tap() - sleep(2) - } - } - - // MARK: - Test Cases - - // MARK: Navigation Tests - - func test01_NavigateToDocumentsScreen() { - navigateToDocumentsTab() - - // Verify we're on documents screen - let navigationTitle = app.navigationBars["Documents & Warranties"] - XCTAssertTrue(navigationTitle.waitForExistence(timeout: 5), "Should navigate to Documents & Warranties screen") - - // Verify tabs are visible - let warrantiesTab = app.buttons["Warranties"] - let documentsTab = app.buttons["Documents"] - XCTAssertTrue(warrantiesTab.exists || documentsTab.exists, "Should see tab switcher") - } - - func test02_SwitchBetweenWarrantiesAndDocuments() { - navigateToDocumentsTab() - - // Start on warranties tab - switchToWarrantiesTab() - sleep(1) - - // Switch to documents tab - switchToDocumentsTab() - sleep(1) - - // Switch back to warranties - switchToWarrantiesTab() - sleep(1) - - // Should not crash and tabs should still exist - let warrantiesTab = app.buttons["Warranties"] - XCTAssertTrue(warrantiesTab.exists, "Tabs should remain functional after switching") - } - - // MARK: Document Creation Tests - - func test03_CreateDocumentWithAllFields() { - navigateToDocumentsTab() - switchToDocumentsTab() - - XCTAssertTrue(openDocumentForm(), "Should open document form") - - let testTitle = "Test Permit \(UUID().uuidString.prefix(8))" - createdDocumentTitles.append(testTitle) - - // Fill all fields - selectProperty() // REQUIRED - Select property first - fillTextField(placeholder: "Title", text: testTitle) - selectDocumentType(type: "Insurance") - fillTextEditor(text: "Test permit description with detailed information") - fillTextField(placeholder: "Tags", text: "construction,permit") - fillTextField(placeholder: "Item Name", text: "Kitchen Renovation") - fillTextField(placeholder: "Location", text: "Main Kitchen") - - XCTAssertTrue(submitForm(), "Should submit form successfully") - - // Verify document appears in list - sleep(2) - let documentCard = app.staticTexts[testTitle] - XCTAssertTrue(documentCard.exists, "Created document should appear in list") - } - - func test04_CreateDocumentWithMinimalFields() { - navigateToDocumentsTab() - switchToDocumentsTab() - - XCTAssertTrue(openDocumentForm(), "Should open document form") - - let testTitle = "Min Doc \(UUID().uuidString.prefix(8))" - createdDocumentTitles.append(testTitle) - - // Fill only required fields - selectProperty() // REQUIRED - Select property first - fillTextField(placeholder: "Title", text: testTitle) - selectDocumentType(type: "Insurance") - - XCTAssertTrue(submitForm(), "Should submit form with minimal fields") - - // Verify document appears - sleep(2) - let documentCard = app.staticTexts[testTitle] - XCTAssertTrue(documentCard.exists, "Document with minimal fields should appear") - } - - func test05_CreateDocumentWithEmptyTitle_ShouldFail() { - navigateToDocumentsTab() - switchToDocumentsTab() - - XCTAssertTrue(openDocumentForm(), "Should open document form") - - // Try to submit without title - selectProperty() // REQUIRED - Select property first - selectDocumentType(type: "Insurance") - - let submitButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save' OR label CONTAINS[c] 'Add'")).firstMatch - - // Submit button should be disabled or show error - if submitButton.exists && submitButton.isEnabled { - submitButton.tap() - sleep(2) - - // Should show error message - let errorMessage = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'required' OR label CONTAINS[c] 'title'")).firstMatch - XCTAssertTrue(errorMessage.exists, "Should show validation error for missing title") - } - - cancelForm() - } - - // MARK: Warranty Creation Tests - - func test06_CreateWarrantyWithAllFields() { - navigateToDocumentsTab() - switchToWarrantiesTab() - - XCTAssertTrue(openDocumentForm(), "Should open warranty form") - - let testTitle = "Test Warranty \(UUID().uuidString.prefix(8))" - createdDocumentTitles.append(testTitle) - - // Fill all warranty fields (including required fields) - selectProperty() // REQUIRED - Select property first - fillTextField(placeholder: "Title", text: testTitle) - selectCategory(category: "Appliances") - fillTextField(placeholder: "Item Name", text: "Dishwasher") // REQUIRED - fillTextField(placeholder: "Provider", text: "Bosch") // REQUIRED - fillTextField(placeholder: "Model", text: "SHPM65Z55N") - fillTextField(placeholder: "Serial", text: "SN123456789") - fillTextField(placeholder: "Provider Contact", text: "1-800-BOSCH-00") - fillTextEditor(text: "Full warranty coverage for 2 years") - - // Select dates - selectDate(dateType: "Start Date", daysFromNow: -30) - selectDate(dateType: "End Date", daysFromNow: 700) // ~2 years - - XCTAssertTrue(submitForm(), "Should submit warranty successfully") - - // Verify warranty appears - sleep(2) - let warrantyCard = app.staticTexts[testTitle] - XCTAssertTrue(warrantyCard.exists, "Created warranty should appear in list") - } - - func test07_CreateWarrantyWithFutureDates() { - navigateToDocumentsTab() - switchToWarrantiesTab() - - XCTAssertTrue(openDocumentForm(), "Should open warranty form") - - let testTitle = "Future Warranty \(UUID().uuidString.prefix(8))" - createdDocumentTitles.append(testTitle) - - selectProperty() // REQUIRED - Select property first - fillTextField(placeholder: "Title", text: testTitle) - selectCategory(category: "HVAC") - fillTextField(placeholder: "Item Name", text: "Air Conditioner") // REQUIRED - fillTextField(placeholder: "Provider", text: "Carrier HVAC") // REQUIRED - - // Set start date in future - selectDate(dateType: "Start Date", daysFromNow: 30) - selectDate(dateType: "End Date", daysFromNow: 400) - - XCTAssertTrue(submitForm(), "Should create warranty with future dates") - - sleep(2) - let warrantyCard = app.staticTexts[testTitle] - XCTAssertTrue(warrantyCard.exists, "Warranty with future dates should be created") - } - - func test08_CreateExpiredWarranty() { - navigateToDocumentsTab() - switchToWarrantiesTab() - - XCTAssertTrue(openDocumentForm(), "Should open warranty form") - - let testTitle = "Expired Warranty \(UUID().uuidString.prefix(8))" - createdDocumentTitles.append(testTitle) - - selectProperty() // REQUIRED - Select property first - fillTextField(placeholder: "Title", text: testTitle) - selectCategory(category: "Plumbing") - fillTextField(placeholder: "Item Name", text: "Water Heater") // REQUIRED - fillTextField(placeholder: "Provider", text: "AO Smith") // REQUIRED - - // Set dates in the past - selectDate(dateType: "Start Date", daysFromNow: -400) - selectDate(dateType: "End Date", daysFromNow: -30) - - XCTAssertTrue(submitForm(), "Should create expired warranty") - - sleep(2) - // Expired warranty might not show with active filter on - // Toggle active filter off to see it - toggleActiveFilter() - sleep(1) - - let warrantyCard = app.staticTexts[testTitle] - XCTAssertTrue(warrantyCard.exists, "Expired warranty should be created and visible when filter is off") - } - - // MARK: Search and Filter Tests - - func test09_SearchDocumentsByTitle() { - navigateToDocumentsTab() - switchToDocumentsTab() - - // Create a test document first - XCTAssertTrue(openDocumentForm(), "Should open form") - let searchableTitle = "Searchable Doc \(UUID().uuidString.prefix(8))" - createdDocumentTitles.append(searchableTitle) - selectProperty() // REQUIRED - Select property first - fillTextField(placeholder: "Title", text: searchableTitle) - selectDocumentType(type: "Insurance") - XCTAssertTrue(submitForm(), "Should create document") - sleep(2) - - // Search for it - searchFor(text: String(searchableTitle.prefix(15))) - - // Should find the document - let foundDocument = app.staticTexts[searchableTitle] - XCTAssertTrue(foundDocument.exists, "Should find document by search") - - clearSearch() - } - - func test10_FilterWarrantiesByCategory() { - navigateToDocumentsTab() - switchToWarrantiesTab() - - // Apply category filter - applyFilter(filterName: "Appliances") - - sleep(2) - - // Should show filter chip or indication - let filterChip = app.staticTexts["Appliances"] - XCTAssertTrue(filterChip.exists || app.buttons["Appliances"].exists, "Should show active category filter") - - // Clear filter - applyFilter(filterName: "All Categories") - } - - func test11_FilterDocumentsByType() { - navigateToDocumentsTab() - switchToDocumentsTab() - - // Apply type filter - applyFilter(filterName: "Permit") - - sleep(2) - - // Should show filter indication - let filterChip = app.staticTexts["Permit"] - XCTAssertTrue(filterChip.exists || app.buttons["Permit"].exists, "Should show active type filter") - - // Clear filter - applyFilter(filterName: "All Types") - } - - func test12_ToggleActiveWarrantiesFilter() { - navigateToDocumentsTab() - switchToWarrantiesTab() - - // Toggle active filter off - toggleActiveFilter() - sleep(1) - - // Toggle it back on - toggleActiveFilter() - sleep(1) - - // Should not crash - let warrantiesTab = app.buttons["Warranties"] - XCTAssertTrue(warrantiesTab.exists, "Active filter toggle should work without crashing") - } - - // MARK: Document Detail Tests - - func test13_ViewDocumentDetail() { - navigateToDocumentsTab() - switchToDocumentsTab() - - // Create a document - XCTAssertTrue(openDocumentForm(), "Should open form") - let testTitle = "Detail Test Doc \(UUID().uuidString.prefix(8))" - createdDocumentTitles.append(testTitle) - selectProperty() // REQUIRED - Select property first - fillTextField(placeholder: "Title", text: testTitle) - selectDocumentType(type: "Insurance") - fillTextEditor(text: "This is a test receipt with details") - XCTAssertTrue(submitForm(), "Should create document") - sleep(2) - - // Tap on the document card - let documentCard = app.staticTexts[testTitle] - XCTAssertTrue(documentCard.exists, "Document should exist in list") - documentCard.tap() - sleep(2) - - // Should show detail screen - let detailTitle = app.staticTexts[testTitle] - XCTAssertTrue(detailTitle.exists, "Should show document detail screen") - - // Go back - let backButton = app.navigationBars.buttons.firstMatch - backButton.tap() - sleep(1) - } - - func test14_ViewWarrantyDetailWithDates() { - navigateToDocumentsTab() - switchToWarrantiesTab() - - // Create a warranty - XCTAssertTrue(openDocumentForm(), "Should open form") - let testTitle = "Warranty Detail Test \(UUID().uuidString.prefix(8))" - createdDocumentTitles.append(testTitle) - selectProperty() // REQUIRED - Select property first - fillTextField(placeholder: "Title", text: testTitle) - selectCategory(category: "Appliances") - fillTextField(placeholder: "Item Name", text: "Test Appliance") // REQUIRED - fillTextField(placeholder: "Provider", text: "Test Company") // REQUIRED - selectDate(dateType: "Start Date", daysFromNow: -30) - selectDate(dateType: "End Date", daysFromNow: 335) - XCTAssertTrue(submitForm(), "Should create warranty") - sleep(2) - - // Tap on warranty - let warrantyCard = app.staticTexts[testTitle] - XCTAssertTrue(warrantyCard.exists, "Warranty should exist") - warrantyCard.tap() - sleep(2) - - // Should show warranty details with dates - let detailScreen = app.staticTexts[testTitle] - XCTAssertTrue(detailScreen.exists, "Should show warranty detail") - - // Look for date information - let dateLabels = app.staticTexts.matching(NSPredicate(format: "label CONTAINS[c] '20' OR label CONTAINS[c] 'Start' OR label CONTAINS[c] 'End'")) - XCTAssertTrue(dateLabels.count > 0, "Should display date information") - - // Go back - app.navigationBars.buttons.firstMatch.tap() - sleep(1) - } - - // MARK: Edit Tests - - func test15_EditDocumentTitle() { - navigateToDocumentsTab() - switchToDocumentsTab() - - // Create document - XCTAssertTrue(openDocumentForm(), "Should open form") - let originalTitle = "Edit Test \(UUID().uuidString.prefix(8))" - createdDocumentTitles.append(originalTitle) - selectProperty() // REQUIRED - Select property first - fillTextField(placeholder: "Title", text: originalTitle) - selectDocumentType(type: "Insurance") - XCTAssertTrue(submitForm(), "Should create document") - sleep(2) - - // Open detail - let documentCard = app.staticTexts[originalTitle] - XCTAssertTrue(documentCard.exists, "Document should exist") - documentCard.tap() - sleep(2) - - // Tap edit button - let editButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Edit'")).firstMatch - if editButton.exists { - editButton.tap() - sleep(2) - - // Change title - let titleField = app.textFields.containing(NSPredicate(format: "value == '\(originalTitle)'")).firstMatch - if titleField.exists { - titleField.tap() - titleField.clearText() - let newTitle = "Edited \(originalTitle)" - titleField.typeText(newTitle) - createdDocumentTitles.append(newTitle) - - XCTAssertTrue(submitForm(), "Should save edited document") - sleep(2) - - // Verify new title appears - let updatedTitle = app.staticTexts[newTitle] - XCTAssertTrue(updatedTitle.exists, "Updated title should appear") - } - } - - // Go back to list - app.navigationBars.buttons.element(boundBy: 0).tap() - sleep(1) - } - - func test16_EditWarrantyDates() { - navigateToDocumentsTab() - switchToWarrantiesTab() - - // Create warranty - XCTAssertTrue(openDocumentForm(), "Should open form") - let testTitle = "Edit Dates Warranty \(UUID().uuidString.prefix(8))" - createdDocumentTitles.append(testTitle) - selectProperty() // REQUIRED - Select property first - fillTextField(placeholder: "Title", text: testTitle) - selectCategory(category: "Electronics") - fillTextField(placeholder: "Item Name", text: "TV") // REQUIRED - fillTextField(placeholder: "Provider", text: "Samsung") // REQUIRED - selectDate(dateType: "Start Date", daysFromNow: -60) - selectDate(dateType: "End Date", daysFromNow: 305) - XCTAssertTrue(submitForm(), "Should create warranty") - sleep(2) - - // Open and edit - let warrantyCard = app.staticTexts[testTitle] - XCTAssertTrue(warrantyCard.exists, "Warranty should exist") - warrantyCard.tap() - sleep(2) - - let editButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Edit'")).firstMatch - if editButton.exists { - editButton.tap() - sleep(2) - - // Change end date to extend warranty - selectDate(dateType: "End Date", daysFromNow: 730) // 2 years - - XCTAssertTrue(submitForm(), "Should save edited warranty dates") - sleep(2) - } - - app.navigationBars.buttons.element(boundBy: 0).tap() - sleep(1) - } - - // MARK: Delete Tests - - func test17_DeleteDocument() { - navigateToDocumentsTab() - switchToDocumentsTab() - - // Create document to delete - XCTAssertTrue(openDocumentForm(), "Should open form") - let deleteTitle = "To Delete \(UUID().uuidString.prefix(8))" - selectProperty() // REQUIRED - Select property first - fillTextField(placeholder: "Title", text: deleteTitle) - selectDocumentType(type: "Insurance") - XCTAssertTrue(submitForm(), "Should create document") - sleep(2) - - // Open detail - let documentCard = app.staticTexts[deleteTitle] - XCTAssertTrue(documentCard.exists, "Document should exist") - documentCard.tap() - sleep(2) - - // Find and tap delete button - let deleteButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Delete' OR label CONTAINS[c] 'trash'")).firstMatch - if deleteButton.exists { - deleteButton.tap() - sleep(1) - - // Confirm deletion - let confirmButton = app.alerts.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Delete' OR label CONTAINS[c] 'Confirm'")).firstMatch - if confirmButton.exists { - confirmButton.tap() - sleep(2) - } - - // Should navigate back to list - sleep(2) - - // Verify document no longer exists - let deletedCard = app.staticTexts[deleteTitle] - XCTAssertFalse(deletedCard.exists, "Deleted document should not appear in list") - } - } - - func test18_DeleteWarranty() { - navigateToDocumentsTab() - switchToWarrantiesTab() - - // Create warranty to delete - XCTAssertTrue(openDocumentForm(), "Should open form") - let deleteTitle = "Warranty to Delete \(UUID().uuidString.prefix(8))" - selectProperty() // REQUIRED - Select property first - fillTextField(placeholder: "Title", text: deleteTitle) - selectCategory(category: "Other") - fillTextField(placeholder: "Item Name", text: "Test Item") // REQUIRED - fillTextField(placeholder: "Provider", text: "Test Provider") // REQUIRED - XCTAssertTrue(submitForm(), "Should create warranty") - sleep(2) - - // Open and delete - let warrantyCard = app.staticTexts[deleteTitle] - XCTAssertTrue(warrantyCard.exists, "Warranty should exist") - warrantyCard.tap() - sleep(2) - - let deleteButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Delete' OR label CONTAINS[c] 'trash'")).firstMatch - if deleteButton.exists { - deleteButton.tap() - sleep(1) - - // Confirm - let confirmButton = app.alerts.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Delete'")).firstMatch - if confirmButton.exists { - confirmButton.tap() - sleep(2) - } - - // Verify deleted - sleep(2) - let deletedCard = app.staticTexts[deleteTitle] - XCTAssertFalse(deletedCard.exists, "Deleted warranty should not appear") - } - } - - // MARK: Edge Cases and Error Handling - - func test19_CancelDocumentCreation() { - navigateToDocumentsTab() - switchToDocumentsTab() - - XCTAssertTrue(openDocumentForm(), "Should open form") - - // Fill some fields - selectProperty() // REQUIRED - Select property first - fillTextField(placeholder: "Title", text: "Cancelled Document") - selectDocumentType(type: "Insurance") - - // Cancel instead of save - cancelForm() - - // Should not appear in list - sleep(2) - let cancelledDoc = app.staticTexts["Cancelled Document"] - XCTAssertFalse(cancelledDoc.exists, "Cancelled document should not be created") - } - - func test20_HandleEmptyDocumentsList() { - navigateToDocumentsTab() - switchToDocumentsTab() - - // Apply very specific filter to get empty list - searchFor(text: "NONEXISTENT_DOCUMENT_12345") - - sleep(2) - - // Should show empty state - let emptyMessage = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'No documents' OR label CONTAINS[c] 'No results' OR label CONTAINS[c] 'empty'")).firstMatch - - // Either empty state exists or no items are shown - let hasNoItems = app.cells.count == 0 - XCTAssertTrue(emptyMessage.exists || hasNoItems, "Should handle empty documents list gracefully") - - clearSearch() - } - - func test21_HandleEmptyWarrantiesList() { - navigateToDocumentsTab() - switchToWarrantiesTab() - - // Search for non-existent warranty - searchFor(text: "NONEXISTENT_WARRANTY_99999") - - sleep(2) - - let emptyMessage = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'No warranties' OR label CONTAINS[c] 'No results' OR label CONTAINS[c] 'empty'")).firstMatch - let hasNoItems = app.cells.count == 0 - XCTAssertTrue(emptyMessage.exists || hasNoItems, "Should handle empty warranties list gracefully") - - clearSearch() - } - - func test22_CreateDocumentWithLongTitle() { - navigateToDocumentsTab() - switchToDocumentsTab() - - XCTAssertTrue(openDocumentForm(), "Should open form") - - let longTitle = "This is a very long document title that exceeds normal length expectations to test how the UI handles lengthy text input " + UUID().uuidString - createdDocumentTitles.append(longTitle) - - selectProperty() // REQUIRED - Select property first - fillTextField(placeholder: "Title", text: longTitle) - selectDocumentType(type: "Insurance") - - XCTAssertTrue(submitForm(), "Should handle long title") - - sleep(2) - // Just verify it was created (partial match) - let partialTitle = String(longTitle.prefix(30)) - let documentExists = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] '\(partialTitle)'")).firstMatch.exists - XCTAssertTrue(documentExists, "Document with long title should be created") - } - - func test23_CreateWarrantyWithSpecialCharacters() { - navigateToDocumentsTab() - switchToWarrantiesTab() - - XCTAssertTrue(openDocumentForm(), "Should open form") - - let specialTitle = "Warranty w/ Special #Chars: @ & $ % \(UUID().uuidString.prefix(8))" - createdDocumentTitles.append(specialTitle) - - selectProperty() // REQUIRED - Select property first - fillTextField(placeholder: "Title", text: specialTitle) - selectCategory(category: "Other") - fillTextField(placeholder: "Item Name", text: "Test @#$ Item") // REQUIRED - fillTextField(placeholder: "Provider", text: "Special & Co.") // REQUIRED - - XCTAssertTrue(submitForm(), "Should handle special characters") - - sleep(2) - let partialTitle = String(specialTitle.prefix(20)) - let warrantyExists = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(partialTitle)'")).firstMatch.exists - XCTAssertTrue(warrantyExists, "Warranty with special characters should be created") - } - - func test24_RapidTabSwitching() { - navigateToDocumentsTab() - - // Rapidly switch between tabs - for _ in 0..<5 { - switchToWarrantiesTab() - usleep(500000) // 0.5 seconds - switchToDocumentsTab() - usleep(500000) // 0.5 seconds - } - - // Should remain stable - let warrantiesTab = app.buttons["Warranties"] - let documentsTab = app.buttons["Documents"] - XCTAssertTrue(warrantiesTab.exists && documentsTab.exists, "Should handle rapid tab switching without crashing") - } - - func test25_MultipleFiltersCombined() { - navigateToDocumentsTab() - switchToWarrantiesTab() - - // Apply multiple filters - toggleActiveFilter() // Turn off active filter - sleep(1) - applyFilter(filterName: "Appliances") - sleep(1) - searchFor(text: "Test") - - sleep(2) - - // Should apply all filters without crashing - let searchField = app.searchFields.firstMatch - XCTAssertTrue(searchField.exists, "Should handle multiple filters simultaneously") - - // Clean up - clearSearch() - sleep(1) - applyFilter(filterName: "All Categories") - sleep(1) - toggleActiveFilter() // Turn active filter back on - } -} - -// MARK: - XCUIElement Extension for Clearing Text - -extension XCUIElement { - func clearText() { - guard let stringValue = self.value as? String else { - return - } - - self.tap() - - let deleteString = String(repeating: XCUIKeyboardKey.delete.rawValue, count: stringValue.count) - self.typeText(deleteString) + XCTAssertTrue(app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.startFreshButton).firstMatch.exists) + XCTAssertTrue(app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.joinExistingButton).firstMatch.exists) } } diff --git a/iosApp/CaseraUITests/Suite9_IntegrationE2ETests.swift b/iosApp/CaseraUITests/Suite9_IntegrationE2ETests.swift index b0b7d50..7952536 100644 --- a/iosApp/CaseraUITests/Suite9_IntegrationE2ETests.swift +++ b/iosApp/CaseraUITests/Suite9_IntegrationE2ETests.swift @@ -1,526 +1,9 @@ import XCTest -/// Comprehensive End-to-End Integration Tests -/// Mirrors the backend integration tests in myCribAPI-go/internal/integration/integration_test.go -/// -/// This test suite covers: -/// 1. Full authentication flow (register, login, logout) -/// 2. Residence CRUD operations -/// 3. Task lifecycle (create, update, mark-in-progress, complete, archive, cancel) -/// 4. Residence sharing between users -/// 5. Cross-user access control -/// -/// IMPORTANT: These tests create real data and require network connectivity. -/// Run with a test server or dev environment (not production). -final class Suite9_IntegrationE2ETests: XCTestCase { - var app: XCUIApplication! - - // Test user credentials - unique per test run - private let timestamp = Int(Date().timeIntervalSince1970) - - private var userAUsername: String { "e2e_usera_\(timestamp)" } - private var userAEmail: String { "e2e_usera_\(timestamp)@test.com" } - private var userAPassword: String { "TestPass123!" } - - private var userBUsername: String { "e2e_userb_\(timestamp)" } - private var userBEmail: String { "e2e_userb_\(timestamp)@test.com" } - private var userBPassword: String { "TestPass456!" } - - /// Fixed verification code used by Go API when DEBUG=true - private let verificationCode = "123456" - - override func setUpWithError() throws { - continueAfterFailure = false - app = XCUIApplication() - app.launch() - } - - override func tearDownWithError() throws { - app = nil - } - - // MARK: - Helper Methods - - private func ensureLoggedOut() { - UITestHelpers.ensureLoggedOut(app: app) - } - - private func login(username: String, password: String) { - UITestHelpers.login(app: app, username: username, password: password) - } - - /// Navigate to a specific tab - private func navigateToTab(_ tabName: String) { - let tab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] '\(tabName)'")).firstMatch - if tab.waitForExistence(timeout: 5) && !tab.isSelected { - tab.tap() - sleep(2) - } - } - - /// Dismiss keyboard by tapping outside (doesn't submit forms) - private func dismissKeyboard() { - let coordinate = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.1)) - coordinate.tap() - Thread.sleep(forTimeInterval: 0.5) - } - - /// Dismiss strong password suggestion if shown - private func dismissStrongPasswordSuggestion() { - let chooseOwnPassword = app.buttons["Choose My Own Password"] - if chooseOwnPassword.waitForExistence(timeout: 1) { - chooseOwnPassword.tap() - return - } - let notNow = app.buttons["Not Now"] - if notNow.exists && notNow.isHittable { - notNow.tap() - } - } - - // MARK: - Test 1: Complete Authentication Flow - // Mirrors TestIntegration_AuthenticationFlow - - func test01_authenticationFlow() { - // Phase 1: Start on login screen - let welcomeText = app.staticTexts["Welcome Back"] - if !welcomeText.waitForExistence(timeout: 5) { - ensureLoggedOut() - } - XCTAssertTrue(welcomeText.waitForExistence(timeout: 10), "Should start on login screen") - - // Phase 2: Navigate to registration - let signUpButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Sign Up'")).firstMatch - XCTAssertTrue(signUpButton.waitForExistence(timeout: 5), "Sign Up button should exist") - signUpButton.tap() - sleep(2) - - // Phase 3: Fill registration form using proper accessibility identifiers - let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField] - XCTAssertTrue(usernameField.waitForExistence(timeout: 5), "Username field should exist") - usernameField.tap() - usernameField.typeText(userAUsername) - - let emailField = app.textFields[AccessibilityIdentifiers.Authentication.registerEmailField] - XCTAssertTrue(emailField.waitForExistence(timeout: 3), "Email field should exist") - emailField.tap() - emailField.typeText(userAEmail) - - // Password field - check both SecureField and TextField - var passwordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerPasswordField] - if !passwordField.exists { - passwordField = app.textFields[AccessibilityIdentifiers.Authentication.registerPasswordField] - } - XCTAssertTrue(passwordField.waitForExistence(timeout: 3), "Password field should exist") - passwordField.tap() - dismissStrongPasswordSuggestion() - passwordField.typeText(userAPassword) - - // Confirm password field - var confirmPasswordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerConfirmPasswordField] - if !confirmPasswordField.exists { - confirmPasswordField = app.textFields[AccessibilityIdentifiers.Authentication.registerConfirmPasswordField] - } - XCTAssertTrue(confirmPasswordField.waitForExistence(timeout: 3), "Confirm password field should exist") - confirmPasswordField.tap() - dismissStrongPasswordSuggestion() - confirmPasswordField.typeText(userAPassword) - - dismissKeyboard() - sleep(1) - - // Phase 4: Submit registration - app.swipeUp() - sleep(1) - - let registerButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton] - XCTAssertTrue(registerButton.waitForExistence(timeout: 5), "Register button should exist") - registerButton.tap() - sleep(3) - - // Phase 5: Handle email verification - let verifyEmailTitle = app.staticTexts["Verify Your Email"] - XCTAssertTrue(verifyEmailTitle.waitForExistence(timeout: 10), "Verification screen must appear after registration") - - sleep(3) - - // Enter verification code - auto-submits when 6 digits entered - let codeField = app.textFields[AccessibilityIdentifiers.Authentication.verificationCodeField] - XCTAssertTrue(codeField.waitForExistence(timeout: 5), "Verification code field must exist") - codeField.tap() - codeField.typeText(verificationCode) - sleep(5) - - // Phase 6: Verify logged in - let tabBar = app.tabBars.firstMatch - XCTAssertTrue(tabBar.waitForExistence(timeout: 15), "Should be logged in after registration") - - // Phase 7: Logout - UITestHelpers.logout(app: app) - - // Phase 8: Login with created credentials - XCTAssertTrue(welcomeText.waitForExistence(timeout: 10), "Should be on login screen after logout") - login(username: userAUsername, password: userAPassword) - - // Phase 9: Verify logged in - XCTAssertTrue(tabBar.waitForExistence(timeout: 10), "Should be logged in after login") - - // Phase 10: Final logout - UITestHelpers.logout(app: app) - XCTAssertTrue(welcomeText.waitForExistence(timeout: 10), "Should be logged out") - } - - // MARK: - Test 2: Residence CRUD Flow - // Mirrors TestIntegration_ResidenceFlow - - func test02_residenceCRUDFlow() { - // Ensure logged in as test user - UITestHelpers.ensureLoggedIn(app: app, username: userAUsername, password: userAPassword) - navigateToTab("Residences") - sleep(2) - - let residenceName = "E2E Test Home \(timestamp)" - - // Phase 1: Create residence - let addButton = app.buttons[AccessibilityIdentifiers.Residence.addButton] - XCTAssertTrue(addButton.waitForExistence(timeout: 5), "Add residence button should exist") - addButton.tap() - sleep(2) - - // Fill form - just tap and type, don't dismiss keyboard between fields - let nameField = app.textFields[AccessibilityIdentifiers.Residence.nameField] - XCTAssertTrue(nameField.waitForExistence(timeout: 5), "Name field should exist") - nameField.tap() - sleep(1) - nameField.typeText(residenceName) - - // Use return key to move to next field or dismiss, then scroll - app.keyboards.buttons["return"].tap() - sleep(1) - - // Scroll to show more fields - app.swipeUp() - sleep(1) - - // Fill street field - let streetField = app.textFields[AccessibilityIdentifiers.Residence.streetAddressField] - if streetField.waitForExistence(timeout: 3) && streetField.isHittable { - streetField.tap() - sleep(1) - streetField.typeText("123 E2E Test St") - app.keyboards.buttons["return"].tap() - sleep(1) - } - - // Fill city field - let cityField = app.textFields[AccessibilityIdentifiers.Residence.cityField] - if cityField.waitForExistence(timeout: 3) && cityField.isHittable { - cityField.tap() - sleep(1) - cityField.typeText("Austin") - app.keyboards.buttons["return"].tap() - sleep(1) - } - - // Fill state field - let stateField = app.textFields[AccessibilityIdentifiers.Residence.stateProvinceField] - if stateField.waitForExistence(timeout: 3) && stateField.isHittable { - stateField.tap() - sleep(1) - stateField.typeText("TX") - app.keyboards.buttons["return"].tap() - sleep(1) - } - - // Fill postal code field - let postalField = app.textFields[AccessibilityIdentifiers.Residence.postalCodeField] - if postalField.waitForExistence(timeout: 3) && postalField.isHittable { - postalField.tap() - sleep(1) - postalField.typeText("78701") - } - - // Dismiss keyboard and scroll to save button - dismissKeyboard() - sleep(1) - app.swipeUp() - sleep(1) - - // Save the residence - let saveButton = app.buttons[AccessibilityIdentifiers.Residence.saveButton] - if saveButton.waitForExistence(timeout: 5) && saveButton.isHittable { - saveButton.tap() - } else { - // Try finding by label as fallback - let saveByLabel = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch - XCTAssertTrue(saveByLabel.waitForExistence(timeout: 5), "Save button should exist") - saveByLabel.tap() - } - sleep(3) - - // Phase 2: Verify residence was created - navigateToTab("Residences") - sleep(2) - let residenceCard = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(residenceName)'")).firstMatch - XCTAssertTrue(residenceCard.waitForExistence(timeout: 10), "Residence '\(residenceName)' should appear in list") - } - - // MARK: - Test 3: Task Lifecycle Flow - // Mirrors TestIntegration_TaskFlow - - func test03_taskLifecycleFlow() { - // Ensure logged in - UITestHelpers.ensureLoggedIn(app: app, username: userAUsername, password: userAPassword) - - // Ensure residence exists first - create one if empty - navigateToTab("Residences") - sleep(2) - - let residenceCards = app.cells - if residenceCards.count == 0 { - // No residences, create one first - createMinimalResidence(name: "Task Test Home \(timestamp)") - sleep(2) - } - - // Navigate to Tasks - navigateToTab("Tasks") - sleep(3) - - let taskTitle = "E2E Task Lifecycle \(timestamp)" - - // Phase 1: Create task - use firstMatch to avoid multiple element issue - let addButton = app.buttons[AccessibilityIdentifiers.Task.addButton].firstMatch - guard addButton.waitForExistence(timeout: 5) else { - XCTFail("Add task button should exist") - return - } - - // Check if button is enabled - guard addButton.isEnabled else { - XCTFail("Add task button should be enabled (requires at least one residence)") - return - } - - addButton.tap() - sleep(2) - - // Fill task form - let titleField = app.textFields[AccessibilityIdentifiers.Task.titleField].firstMatch - XCTAssertTrue(titleField.waitForExistence(timeout: 5), "Task title field should exist") - titleField.tap() - sleep(1) - titleField.typeText(taskTitle) - - dismissKeyboard() - sleep(1) - app.swipeUp() - sleep(1) - - // Save the task - let saveTaskButton = app.buttons[AccessibilityIdentifiers.Task.saveButton].firstMatch - if saveTaskButton.waitForExistence(timeout: 5) && saveTaskButton.isHittable { - saveTaskButton.tap() - } else { - let saveByLabel = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save' OR label CONTAINS[c] 'Add Task' OR label CONTAINS[c] 'Create'")).firstMatch - XCTAssertTrue(saveByLabel.exists, "Save/Create button should exist") - saveByLabel.tap() - } - sleep(3) - - // Phase 2: Verify task was created - navigateToTab("Tasks") - sleep(2) - let taskCard = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(taskTitle)'")).firstMatch - XCTAssertTrue(taskCard.waitForExistence(timeout: 10), "Task '\(taskTitle)' should appear in task list") - } - - // MARK: - Test 4: Kanban Column Distribution - // Mirrors TestIntegration_TasksByResidenceKanban - - func test04_kanbanColumnDistribution() { - UITestHelpers.ensureLoggedIn(app: app, username: userAUsername, password: userAPassword) - navigateToTab("Tasks") - sleep(3) - - // Verify tasks screen is showing - let tasksTitle = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch - let kanbanExists = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Overdue' OR label CONTAINS[c] 'Upcoming' OR label CONTAINS[c] 'In Progress'")).firstMatch.exists - - XCTAssertTrue(kanbanExists || tasksTitle.exists, "Tasks screen should be visible") - } - - // MARK: - Test 5: Cross-User Access Control - // Mirrors TestIntegration_CrossUserAccessDenied - - func test05_crossUserAccessControl() { - UITestHelpers.ensureLoggedIn(app: app, username: userAUsername, password: userAPassword) - - // Verify user can access their residences tab - navigateToTab("Residences") - sleep(2) - - let residencesVisible = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch.isSelected - XCTAssertTrue(residencesVisible, "User should be able to access Residences tab") - - // Verify user can access their tasks tab - navigateToTab("Tasks") - sleep(2) - - let tasksAccessible = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch.isSelected - XCTAssertTrue(tasksAccessible, "User should be able to access Tasks tab") - } - - // MARK: - Test 6: Lookup Data Endpoints - // Mirrors TestIntegration_LookupEndpoints - - func test06_lookupDataAvailable() { - UITestHelpers.ensureLoggedIn(app: app, username: userAUsername, password: userAPassword) - - // Navigate to add residence to check residence types are loaded - navigateToTab("Residences") - sleep(2) - - let addButton = app.buttons[AccessibilityIdentifiers.Residence.addButton] - if addButton.waitForExistence(timeout: 5) { - addButton.tap() - sleep(2) - - // Check property type picker exists (indicates lookups loaded) - let propertyTypePicker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Property Type' OR label CONTAINS[c] 'Type'")).firstMatch - let pickerExists = propertyTypePicker.exists - - // Cancel form - let cancelButton = app.buttons[AccessibilityIdentifiers.Residence.formCancelButton] - if cancelButton.exists { - cancelButton.tap() - } else { - let cancelByLabel = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Cancel'")).firstMatch - if cancelByLabel.exists { - cancelByLabel.tap() - } - } - - XCTAssertTrue(pickerExists, "Property type picker should exist (lookups loaded)") - } - } - - // MARK: - Test 7: Residence Sharing Flow - // Mirrors TestIntegration_ResidenceSharingFlow - - func test07_residenceSharingUIElements() { - UITestHelpers.ensureLoggedIn(app: app, username: userAUsername, password: userAPassword) - navigateToTab("Residences") - sleep(2) - - // Find any residence to check sharing UI - let residenceCard = app.cells.firstMatch - if residenceCard.waitForExistence(timeout: 5) { - residenceCard.tap() - sleep(2) - - // Look for share button in residence details - let shareButton = app.buttons[AccessibilityIdentifiers.Residence.shareButton] - let manageUsersButton = app.buttons[AccessibilityIdentifiers.Residence.manageUsersButton] - - // Note: Share functionality may not be visible depending on user permissions - // This test just verifies we can navigate to residence details - - // Navigate back - let backButton = app.navigationBars.buttons.element(boundBy: 0) - if backButton.exists && backButton.isHittable { - backButton.tap() - sleep(1) - } - } - } - - // MARK: - Helper: Create Minimal Residence - - private func createMinimalResidence(name: String) { - let addButton = app.buttons[AccessibilityIdentifiers.Residence.addButton] - guard addButton.waitForExistence(timeout: 5) else { return } - - addButton.tap() - sleep(2) - - // Fill name field - let nameField = app.textFields[AccessibilityIdentifiers.Residence.nameField] - if nameField.waitForExistence(timeout: 5) { - nameField.tap() - sleep(1) - nameField.typeText(name) - app.keyboards.buttons["return"].tap() - sleep(1) - } - - // Scroll to show address fields - app.swipeUp() - sleep(1) - - // Fill street field - let streetField = app.textFields[AccessibilityIdentifiers.Residence.streetAddressField] - if streetField.waitForExistence(timeout: 3) && streetField.isHittable { - streetField.tap() - sleep(1) - streetField.typeText("123 Test St") - app.keyboards.buttons["return"].tap() - sleep(1) - } - - // Fill city field - let cityField = app.textFields[AccessibilityIdentifiers.Residence.cityField] - if cityField.waitForExistence(timeout: 3) && cityField.isHittable { - cityField.tap() - sleep(1) - cityField.typeText("Austin") - app.keyboards.buttons["return"].tap() - sleep(1) - } - - // Fill state field - let stateField = app.textFields[AccessibilityIdentifiers.Residence.stateProvinceField] - if stateField.waitForExistence(timeout: 3) && stateField.isHittable { - stateField.tap() - sleep(1) - stateField.typeText("TX") - app.keyboards.buttons["return"].tap() - sleep(1) - } - - // Fill postal code field - let postalField = app.textFields[AccessibilityIdentifiers.Residence.postalCodeField] - if postalField.waitForExistence(timeout: 3) && postalField.isHittable { - postalField.tap() - sleep(1) - postalField.typeText("78701") - } - - dismissKeyboard() - sleep(1) - app.swipeUp() - sleep(1) - - // Save - let saveButton = app.buttons[AccessibilityIdentifiers.Residence.saveButton] - if saveButton.waitForExistence(timeout: 5) && saveButton.isHittable { - saveButton.tap() - } else { - let saveByLabel = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch - if saveByLabel.exists { - saveByLabel.tap() - } - } - sleep(3) - } - - // MARK: - Helper: Find Add Task Button - - private func findAddTaskButton() -> XCUIElement { - let addButton = app.buttons[AccessibilityIdentifiers.Task.addButton] - if addButton.exists { - return addButton - } - return app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Add Task' OR label CONTAINS[c] 'New Task'")).firstMatch +final class Suite9_IntegrationE2ETests: BaseUITestCase { + func testSuite9_StartFreshAndExpandEmailSignup() { + let createAccount = TestFlows.navigateStartFreshToCreateAccount(app: app, residenceName: "Suite9 House") + createAccount.expandEmailSignup() + createAccount.waitForCreateAccountButton(timeout: defaultTimeout) } } diff --git a/iosApp/CaseraUITests/Tests/AccessibilityTests.swift b/iosApp/CaseraUITests/Tests/AccessibilityTests.swift new file mode 100644 index 0000000..aaa5600 --- /dev/null +++ b/iosApp/CaseraUITests/Tests/AccessibilityTests.swift @@ -0,0 +1,33 @@ +import XCTest + +final class AccessibilityTests: BaseUITestCase { + func testA001_OnboardingPrimaryControlsAreReachable() { + let welcome = OnboardingWelcomeScreen(app: app) + welcome.waitForLoad() + + app.buttons[UITestID.Onboarding.startFreshButton].waitUntilHittable(timeout: defaultTimeout) + app.buttons[UITestID.Onboarding.joinExistingButton].waitUntilHittable(timeout: defaultTimeout) + app.buttons[UITestID.Onboarding.loginButton].waitUntilHittable(timeout: defaultTimeout) + } + + func testA002_LoginControlsRemainOperable() { + let login = TestFlows.navigateToLoginFromOnboarding(app: app) + + app.textFields[UITestID.Auth.usernameField].waitUntilHittable(timeout: defaultTimeout) + app.secureTextFields[UITestID.Auth.passwordField].waitUntilHittable(timeout: defaultTimeout) + app.buttons[UITestID.Auth.loginButton].waitUntilHittable(timeout: defaultTimeout) + + login.tapPasswordVisibilityToggle() + login.assertPasswordFieldVisible() + } + + func testA003_CoreControlsExposeIdentifiers() { + let login = TestFlows.navigateToLoginFromOnboarding(app: app) + _ = login + + XCTAssertTrue(app.textFields[UITestID.Auth.usernameField].exists) + XCTAssertTrue(app.secureTextFields[UITestID.Auth.passwordField].exists || app.textFields[UITestID.Auth.passwordField].exists) + XCTAssertTrue(app.buttons[UITestID.Auth.signUpButton].exists) + XCTAssertTrue(app.buttons[UITestID.Auth.forgotPasswordButton].exists) + } +} diff --git a/iosApp/CaseraUITests/Tests/AppLaunchTests.swift b/iosApp/CaseraUITests/Tests/AppLaunchTests.swift new file mode 100644 index 0000000..57f0c2b --- /dev/null +++ b/iosApp/CaseraUITests/Tests/AppLaunchTests.swift @@ -0,0 +1,19 @@ +import XCTest + +final class AppLaunchTests: BaseUITestCase { + func testF001_ColdLaunchShowsOnboardingWelcome() { + RootScreen(app: app).waitForReady(timeout: defaultTimeout) + + let welcome = OnboardingWelcomeScreen(app: app) + welcome.waitForLoad(timeout: defaultTimeout) + } + + func testF002_ColdLaunchShowsPrimaryOnboardingActions() { + let welcome = OnboardingWelcomeScreen(app: app) + welcome.waitForLoad() + + XCTAssertTrue(app.buttons[UITestID.Onboarding.startFreshButton].exists) + XCTAssertTrue(app.buttons[UITestID.Onboarding.joinExistingButton].exists) + XCTAssertTrue(app.buttons[UITestID.Onboarding.loginButton].exists) + } +} diff --git a/iosApp/CaseraUITests/Tests/AuthenticationTests.swift b/iosApp/CaseraUITests/Tests/AuthenticationTests.swift new file mode 100644 index 0000000..b90e8cb --- /dev/null +++ b/iosApp/CaseraUITests/Tests/AuthenticationTests.swift @@ -0,0 +1,31 @@ +import XCTest + +final class AuthenticationTests: BaseUITestCase { + func testF201_OnboardingLoginEntryShowsLoginScreen() { + let login = TestFlows.navigateToLoginFromOnboarding(app: app) + login.waitForLoad(timeout: defaultTimeout) + } + + func testF202_LoginScreenCanTogglePasswordVisibility() { + let login = TestFlows.navigateToLoginFromOnboarding(app: app) + login.enterUsername("u") + login.enterPassword("p") + login.tapPasswordVisibilityToggle() + login.assertPasswordFieldVisible() + } + + func testF203_RegisterSheetCanOpenAndDismiss() { + let register = TestFlows.openRegisterFromLogin(app: app) + register.tapCancel() + + let login = LoginScreen(app: app) + login.waitForLoad(timeout: defaultTimeout) + } + + func testF204_RegisterFormAcceptsInput() { + let register = TestFlows.openRegisterFromLogin(app: app) + register.waitForLoad(timeout: defaultTimeout) + + XCTAssertTrue(app.buttons[UITestID.Auth.registerButton].exists) + } +} diff --git a/iosApp/CaseraUITests/Tests/OnboardingTests.swift b/iosApp/CaseraUITests/Tests/OnboardingTests.swift new file mode 100644 index 0000000..e32e14b --- /dev/null +++ b/iosApp/CaseraUITests/Tests/OnboardingTests.swift @@ -0,0 +1,33 @@ +import XCTest + +final class OnboardingTests: BaseUITestCase { + func testF101_StartFreshFlowReachesCreateAccount() { + let createAccount = TestFlows.navigateStartFreshToCreateAccount(app: app, residenceName: "Blueprint House") + createAccount.waitForLoad(timeout: defaultTimeout) + } + + func testF102_JoinExistingFlowGoesToCreateAccount() { + let welcome = OnboardingWelcomeScreen(app: app) + welcome.waitForLoad() + welcome.tapJoinExisting() + + let createAccount = OnboardingCreateAccountScreen(app: app) + createAccount.waitForLoad(timeout: defaultTimeout) + } + + func testF103_BackNavigationFromNameResidenceReturnsToValueProps() { + let welcome = OnboardingWelcomeScreen(app: app) + welcome.waitForLoad() + welcome.tapStartFresh() + + let valueProps = OnboardingValuePropsScreen(app: app) + valueProps.waitForLoad() + valueProps.tapContinue() + + let nameResidence = OnboardingNameResidenceScreen(app: app) + nameResidence.waitForLoad() + nameResidence.tapBack() + + XCTAssertTrue(app.otherElements[UITestID.Root.onboarding].waitForExistence(timeout: defaultTimeout)) + } +} diff --git a/iosApp/CaseraUITests/Tests/StabilityTests.swift b/iosApp/CaseraUITests/Tests/StabilityTests.swift new file mode 100644 index 0000000..d113652 --- /dev/null +++ b/iosApp/CaseraUITests/Tests/StabilityTests.swift @@ -0,0 +1,39 @@ +import XCTest + +final class StabilityTests: BaseUITestCase { + func testP001_RapidOnboardingNavigationDoesNotCrash() { + for _ in 0..<3 { + let welcome = OnboardingWelcomeScreen(app: app) + welcome.waitForLoad(timeout: defaultTimeout) + welcome.tapStartFresh() + + let valueProps = OnboardingValuePropsScreen(app: app) + valueProps.waitForLoad(timeout: defaultTimeout) + valueProps.tapBack() + + welcome.waitForLoad(timeout: defaultTimeout) + } + } + + func testP002_RepeatedForwardNavigationRemainsResponsive() { + for index in 0..<3 { + let welcome = OnboardingWelcomeScreen(app: app) + welcome.waitForLoad(timeout: defaultTimeout) + welcome.tapStartFresh() + + let valueProps = OnboardingValuePropsScreen(app: app) + valueProps.waitForLoad(timeout: defaultTimeout) + valueProps.tapContinue() + + let nameResidence = OnboardingNameResidenceScreen(app: app) + nameResidence.waitForLoad(timeout: defaultTimeout) + nameResidence.enterResidenceName("Stress Home \(index)") + nameResidence.tapBack() + + valueProps.waitForLoad(timeout: defaultTimeout) + valueProps.tapBack() + + welcome.waitForLoad(timeout: defaultTimeout) + } + } +} diff --git a/iosApp/CaseraUITests/UITestHelpers.swift b/iosApp/CaseraUITests/UITestHelpers.swift deleted file mode 100644 index c5bd3e9..0000000 --- a/iosApp/CaseraUITests/UITestHelpers.swift +++ /dev/null @@ -1,119 +0,0 @@ -import XCTest - -/// Reusable helper functions for UI tests -struct UITestHelpers { - - // MARK: - Authentication Helpers - - /// Logs out the user if they are currently logged in - /// - Parameter app: The XCUIApplication instance - static func logout(app: XCUIApplication) { - sleep(2) - - // Check if already logged out (login screen visible) - let welcomeText = app.staticTexts["Welcome Back"] - if welcomeText.exists { - // Already logged out - return - } - - // Check if we have a tab bar (logged in state) - let tabBar = app.tabBars.firstMatch - guard tabBar.exists else { return } - - // Navigate to Residences tab first - let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch - if residencesTab.exists { - residencesTab.tap() - sleep(1) - } - - // Tap settings button - let settingsButton = app.buttons[AccessibilityIdentifiers.Navigation.settingsButton] - if settingsButton.waitForExistence(timeout: 3) && settingsButton.isHittable { - settingsButton.tap() - sleep(1) - } - - // Find and tap logout button - let logoutButton = app.buttons[AccessibilityIdentifiers.Profile.logoutButton] - if logoutButton.waitForExistence(timeout: 3) { - logoutButton.tap() - sleep(1) - - // Confirm logout in alert if present - specifically target the alert's button - let alert = app.alerts.firstMatch - if alert.waitForExistence(timeout: 2) { - let confirmLogout = alert.buttons["Log Out"] - if confirmLogout.exists { - confirmLogout.tap() - } - } - } - - sleep(2) - - // Verify we're back on login screen - XCTAssertTrue(welcomeText.waitForExistence(timeout: 5), "Failed to log out - Welcome Back screen should appear after logout") - } - - /// Logs in a user with the provided credentials - /// - Parameters: - /// - app: The XCUIApplication instance - /// - username: The username/email to use for login - /// - password: The password to use for login - static func login(app: XCUIApplication, username: String, password: String) { - // Find username field by accessibility identifier - let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.usernameField] - XCTAssertTrue(usernameField.waitForExistence(timeout: 5), "Username field should exist") - usernameField.tap() - usernameField.typeText(username) - - // Find password field - it could be TextField (if visible) or SecureField - var passwordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.passwordField] - if !passwordField.exists { - passwordField = app.textFields[AccessibilityIdentifiers.Authentication.passwordField] - } - XCTAssertTrue(passwordField.waitForExistence(timeout: 3), "Password field should exist") - passwordField.tap() - passwordField.typeText(password) - - // Find and tap login button - let loginButton = app.buttons[AccessibilityIdentifiers.Authentication.loginButton] - XCTAssertTrue(loginButton.waitForExistence(timeout: 3), "Login button should exist") - loginButton.tap() - - // Wait for login to complete - sleep(3) - } - - /// Ensures the user is logged out before running a test - /// - Parameter app: The XCUIApplication instance - static func ensureLoggedOut(app: XCUIApplication) { - sleep(2) - logout(app: app) - } - - /// Ensures the user is logged in with test credentials before running a test - /// - Parameter app: The XCUIApplication instance - /// - Parameter username: Optional username (defaults to "testuser") - /// - Parameter password: Optional password (defaults to "TestPass123!") - static func ensureLoggedIn(app: XCUIApplication, username: String = "testuser", password: String = "TestPass123!") { - sleep(2) - - // Check if already logged in (tab bar visible) - let tabBar = app.tabBars.firstMatch - if tabBar.exists { - return // Already logged in - } - - // Check if on login screen - let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.usernameField] - if usernameField.waitForExistence(timeout: 5) { - login(app: app, username: username, password: password) - - // Wait for main screen to appear - _ = tabBar.waitForExistence(timeout: 10) - } - } -} diff --git a/iosApp/XCUITest-Authoring.md b/iosApp/XCUITest-Authoring.md new file mode 100644 index 0000000..72a80db --- /dev/null +++ b/iosApp/XCUITest-Authoring.md @@ -0,0 +1,24 @@ +# XCUITest Authoring + +## Required Architecture +- Put shared test infrastructure in `/Users/treyt/Desktop/code/MyCribKMM/iosApp/CaseraUITests/Framework`. +- Put feature suites in `/Users/treyt/Desktop/code/MyCribKMM/iosApp/CaseraUITests/Tests`. +- Every test suite inherits `BaseUITestCase`. +- Reusable multi-step setup belongs in `TestFlows`. +- UI interactions should go through screen objects in `ScreenObjects.swift`. + +## Runtime Contract +- Launch args are standardized in `BaseUITestCase`: + - `--ui-testing` + - `--disable-animations` + - `--reset-state` +- App-side behavior for UI test mode is implemented in `/Users/treyt/Desktop/code/MyCribKMM/iosApp/iosApp/Helpers/UITestRuntime.swift`. + +## Naming +- Test method naming format: `test_()`. +- Case IDs should stay stable once committed. + +## Waiting and Flake Rules +- Use helper waits from `BaseUITestCase` extensions. +- Do not add blind `sleep()`. +- Prefer stable accessibility identifiers over visible text selectors. diff --git a/iosApp/XCUITestSuiteTemplate.swift b/iosApp/XCUITestSuiteTemplate.swift new file mode 100644 index 0000000..920fa82 --- /dev/null +++ b/iosApp/XCUITestSuiteTemplate.swift @@ -0,0 +1,16 @@ +import XCTest + +final class TemplateFeatureTests: BaseUITestCase { + func testF900_TemplateBehavior() { + // Arrange + let root = RootScreen(app: app) + root.waitForReady(timeout: defaultTimeout) + + // Act + let welcome = OnboardingWelcomeScreen(app: app) + welcome.waitForLoad(timeout: defaultTimeout) + + // Assert + XCTAssertTrue(app.buttons[UITestID.Onboarding.startFreshButton].exists) + } +} diff --git a/iosApp/iosApp/Helpers/UITestRuntime.swift b/iosApp/iosApp/Helpers/UITestRuntime.swift new file mode 100644 index 0000000..af574fc --- /dev/null +++ b/iosApp/iosApp/Helpers/UITestRuntime.swift @@ -0,0 +1,44 @@ +import Foundation +import UIKit +import ComposeApp + +/// Runtime contract between the app and XCUITests. +enum UITestRuntime { + static let uiTestingFlag = "--ui-testing" + static let disableAnimationsFlag = "--disable-animations" + static let resetStateFlag = "--reset-state" + + static var launchArguments: [String] { + ProcessInfo.processInfo.arguments + } + + static var isEnabled: Bool { + launchArguments.contains(uiTestingFlag) + } + + static var shouldDisableAnimations: Bool { + isEnabled && launchArguments.contains(disableAnimationsFlag) + } + + static var shouldResetState: Bool { + isEnabled && launchArguments.contains(resetStateFlag) + } + + static func configureForLaunch() { + guard isEnabled else { return } + + if shouldDisableAnimations { + UIView.setAnimationsEnabled(false) + } + + UserDefaults.standard.set(true, forKey: "ui_testing_mode") + } + + static func resetStateIfRequested() { + guard shouldResetState else { return } + + DataManager.shared.clear() + OnboardingState.shared.reset() + ThemeManager.shared.currentTheme = .bright + } +} diff --git a/iosApp/iosApp/Onboarding/OnboardingCoordinator.swift b/iosApp/iosApp/Onboarding/OnboardingCoordinator.swift index 7170070..ada9da4 100644 --- a/iosApp/iosApp/Onboarding/OnboardingCoordinator.swift +++ b/iosApp/iosApp/Onboarding/OnboardingCoordinator.swift @@ -195,6 +195,7 @@ struct OnboardingCoordinator: View { .font(.title2) .foregroundColor(Color.appPrimary) } + .accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.backButton) .opacity(showBackButton ? 1 : 0) .disabled(!showBackButton) @@ -203,6 +204,7 @@ struct OnboardingCoordinator: View { // Progress indicator if showProgressIndicator { OnboardingProgressIndicator(currentStep: currentProgressStep, totalSteps: 5) + .accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.progressIndicator) } Spacer() @@ -214,6 +216,7 @@ struct OnboardingCoordinator: View { .fontWeight(.medium) .foregroundColor(Color.appTextSecondary) } + .accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.skipButton) .opacity(showSkipButton ? 1 : 0) .disabled(!showSkipButton) } diff --git a/iosApp/iosApp/Onboarding/OnboardingValuePropsView.swift b/iosApp/iosApp/Onboarding/OnboardingValuePropsView.swift index 7801a1c..014cdcd 100644 --- a/iosApp/iosApp/Onboarding/OnboardingValuePropsView.swift +++ b/iosApp/iosApp/Onboarding/OnboardingValuePropsView.swift @@ -68,6 +68,7 @@ struct OnboardingValuePropsContent: View { .padding(.horizontal, 20) } } + .accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.valuePropsTitle) .tabViewStyle(.page(indexDisplayMode: .never)) .frame(maxHeight: .infinity) @@ -104,6 +105,7 @@ struct OnboardingValuePropsContent: View { .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) .naturalShadow(.medium) } + .accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.valuePropsNextButton) .padding(.horizontal, OrganicSpacing.comfortable) .padding(.bottom, OrganicSpacing.airy) } diff --git a/iosApp/iosApp/Onboarding/OnboardingWelcomeView.swift b/iosApp/iosApp/Onboarding/OnboardingWelcomeView.swift index 87d4211..8462525 100644 --- a/iosApp/iosApp/Onboarding/OnboardingWelcomeView.swift +++ b/iosApp/iosApp/Onboarding/OnboardingWelcomeView.swift @@ -177,6 +177,11 @@ struct OnboardingWelcomeView: View { .opacity(0.5) .padding(.bottom, 20) } + + // Deterministic marker for UI tests. + Color.clear + .frame(width: 1, height: 1) + .accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.welcomeTitle) } .sheet(isPresented: $showingLoginSheet) { LoginView(onLoginSuccess: { diff --git a/iosApp/iosApp/Residence/ResidenceDetailView.swift b/iosApp/iosApp/Residence/ResidenceDetailView.swift index 13f9cd5..ed93c63 100644 --- a/iosApp/iosApp/Residence/ResidenceDetailView.swift +++ b/iosApp/iosApp/Residence/ResidenceDetailView.swift @@ -246,6 +246,8 @@ private extension ResidenceDetailView { selectedTaskForComplete: $selectedTaskForComplete, selectedTaskForArchive: $selectedTaskForArchive, showArchiveConfirmation: $showArchiveConfirmation, + selectedTaskForCancel: $selectedTaskForCancel, + showCancelConfirmation: $showCancelConfirmation, reloadTasks: { loadResidenceTasks(forceRefresh: true) } ) } else if isLoadingTasks { @@ -495,6 +497,8 @@ private struct TasksSectionContainer: View { @Binding var selectedTaskForComplete: TaskResponse? @Binding var selectedTaskForArchive: TaskResponse? @Binding var showArchiveConfirmation: Bool + @Binding var selectedTaskForCancel: TaskResponse? + @Binding var showCancelConfirmation: Bool let reloadTasks: () -> Void diff --git a/iosApp/iosApp/RootView.swift b/iosApp/iosApp/RootView.swift index 97fa2b3..25cdece 100644 --- a/iosApp/iosApp/RootView.swift +++ b/iosApp/iosApp/RootView.swift @@ -14,6 +14,13 @@ class AuthenticationManager: ObservableObject { } func checkAuthenticationStatus() { + if UITestRuntime.isEnabled { + isAuthenticated = DataManager.shared.isAuthenticated() + isVerified = isAuthenticated + isCheckingAuth = false + return + } + isCheckingAuth = true // Check if token exists via DataManager (single source of truth) @@ -69,6 +76,8 @@ class AuthenticationManager: ObservableObject { isAuthenticated = true isVerified = verified + guard !UITestRuntime.isEnabled else { return } + // Register device for push notifications now that user is authenticated PushNotificationManager.shared.registerDeviceAfterLogin() } @@ -76,6 +85,8 @@ class AuthenticationManager: ObservableObject { func markVerified() { isVerified = true + guard !UITestRuntime.isEnabled else { return } + // Lookups are already initialized at app start or during login/register // Just verify subscription entitlements after user becomes verified Task { @@ -118,38 +129,59 @@ struct RootView: View { @State private var refreshID = UUID() var body: some View { - Group { - if authManager.isCheckingAuth { - // Show loading while checking auth status - loadingView - } else if !onboardingState.hasCompletedOnboarding { - // Show onboarding for first-time users (includes auth + verification steps) - // This takes precedence because we need to finish the onboarding flow - OnboardingCoordinator(onComplete: { - // Onboarding complete - mark verified and refresh the view - authManager.markVerified() - refreshID = UUID() - }) - } else if !authManager.isAuthenticated { - // Show login screen for returning users - LoginView() - } else if !authManager.isVerified { - // Show email verification screen (for returning users who haven't verified) - VerifyEmailView( - onVerifySuccess: { - authManager.markVerified() - }, - onLogout: { - authManager.logout() + ZStack(alignment: .topLeading) { + Group { + if authManager.isCheckingAuth { + // Show loading while checking auth status + loadingView + .accessibilityIdentifier(AccessibilityIdentifiers.Common.loadingIndicator) + } else if !onboardingState.hasCompletedOnboarding { + // Show onboarding for first-time users (includes auth + verification steps) + // This takes precedence because we need to finish the onboarding flow + ZStack(alignment: .topLeading) { + OnboardingCoordinator(onComplete: { + // Onboarding complete - mark verified and refresh the view + authManager.markVerified() + refreshID = UUID() + }) + Color.clear + .frame(width: 1, height: 1) + .accessibilityIdentifier("ui.root.onboarding") } - ) - } else { - // Show main app - MainTabView(refreshID: refreshID) - .onChange(of: themeManager.currentTheme) { _ in - refreshID = UUID() + } else if !authManager.isAuthenticated { + // Show login screen for returning users + ZStack(alignment: .topLeading) { + LoginView() + Color.clear + .frame(width: 1, height: 1) + .accessibilityIdentifier("ui.root.login") } + } else if !authManager.isVerified { + // Show email verification screen (for returning users who haven't verified) + VerifyEmailView( + onVerifySuccess: { + authManager.markVerified() + }, + onLogout: { + authManager.logout() + } + ) + } else { + // Show main app + ZStack(alignment: .topLeading) { + MainTabView(refreshID: refreshID) + .onChange(of: themeManager.currentTheme) { _ in + refreshID = UUID() + } + Color.clear + .frame(width: 1, height: 1) + .accessibilityIdentifier("ui.root.mainTabs") + } + } } + Color.clear + .frame(width: 1, height: 1) + .accessibilityIdentifier("ui.app.ready") } } diff --git a/iosApp/iosApp/Shared/Components/FormComponents.swift b/iosApp/iosApp/Shared/Components/FormComponents.swift index 2165030..818784a 100644 --- a/iosApp/iosApp/Shared/Components/FormComponents.swift +++ b/iosApp/iosApp/Shared/Components/FormComponents.swift @@ -332,6 +332,7 @@ struct SecureIconTextField: View { .font(.system(size: 16, weight: .medium)) .foregroundColor(Color.appTextSecondary) } + .accessibilityIdentifier(AccessibilityIdentifiers.Authentication.passwordVisibilityToggle) } .padding(16) .background(Color.appBackgroundPrimary.opacity(0.5)) diff --git a/iosApp/iosApp/iOSApp.swift b/iosApp/iosApp/iOSApp.swift index 5069e2b..30e9b3b 100644 --- a/iosApp/iosApp/iOSApp.swift +++ b/iosApp/iosApp/iOSApp.swift @@ -21,6 +21,8 @@ struct iOSApp: App { } init() { + UITestRuntime.configureForLaunch() + // Initialize DataManager with platform-specific managers // This must be done before any other operations that access DataManager DataManager.shared.initialize( @@ -29,18 +31,28 @@ struct iOSApp: App { persistenceMgr: PersistenceManager() ) + if UITestRuntime.isEnabled { + Task { @MainActor in + UITestRuntime.resetStateIfRequested() + } + } + // Initialize TokenStorage once at app startup (legacy support) TokenStorage.shared.initialize(manager: TokenManager.Companion.shared.getInstance()) - // Initialize PostHog Analytics - PostHogAnalytics.shared.initialize() + if !UITestRuntime.isEnabled { + // Initialize PostHog Analytics + PostHogAnalytics.shared.initialize() + } // Initialize lookups at app start (public endpoints, no auth required) // This fetches /static_data/ and /upgrade-triggers/ immediately - Task { - print("🚀 Initializing lookups at app start...") - _ = try? await APILayer.shared.initializeLookups() - print("✅ Lookups initialized") + if !UITestRuntime.isEnabled { + Task { + print("🚀 Initializing lookups at app start...") + _ = try? await APILayer.shared.initializeLookups() + print("✅ Lookups initialized") + } } } @@ -54,6 +66,8 @@ struct iOSApp: App { handleIncomingURL(url: url) } .onChange(of: scenePhase) { newPhase in + guard !UITestRuntime.isEnabled else { return } + if newPhase == .active { // Sync auth token to widget if user is logged in // This ensures widget has credentials even if user logged in before widget support was added