import XCTest /// Comprehensive End-to-End Integration Tests /// Mirrors the backend integration tests in honeyDueAPI-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: AuthenticatedUITestCase { override var needsAPISession: Bool { true } // Unique ID for test data names private let testRunId = Int(Date().timeIntervalSince1970) // API-created test user for tests 02-07 private var apiUser: TestSession! override func setUpWithError() throws { // Create a unique test user via API (fast, reliable, no keyboard issues) guard TestAccountAPIClient.isBackendReachable() else { throw XCTSkip("Backend not reachable") } guard let user = TestAccountManager.createVerifiedAccount() else { throw XCTSkip("Could not create test user via API") } apiUser = user // Use the API-created user for UI login _overrideCredentials = (user.username, user.password) try super.setUpWithError() } private var _overrideCredentials: (String, String)? override var testCredentials: (username: String, password: String) { _overrideCredentials ?? ("testuser", "TestPass123!") } // MARK: - Helper Methods /// 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() { // This test verifies the full auth lifecycle via API // (UI registration is tested by Suite1_RegistrationTests) let timestamp = Int(Date().timeIntervalSince1970) let testUser = "e2e_auth_\(testRunId)" let testEmail = "e2e_auth_\(testRunId)@test.com" let testPassword = "TestPass123!" // Phase 1: Create user via API guard let session = TestAccountAPIClient.createVerifiedAccount( username: testUser, email: testEmail, password: testPassword ) else { XCTFail("Could not create test user via API") return } // Phase 2: Logout current user and login as new user via UI UITestHelpers.ensureLoggedOut(app: app) let welcomeText = app.textFields[AccessibilityIdentifiers.Authentication.usernameField] XCTAssertTrue(welcomeText.waitForExistence(timeout: 10), "Should be on login screen") UITestHelpers.login(app: app, username: testUser, password: testPassword) // Phase 3: Verify logged in let tabBar = app.tabBars.firstMatch XCTAssertTrue(tabBar.waitForExistence(timeout: 15), "Should be logged in after login") // Phase 4: Logout UITestHelpers.logout(app: app) XCTAssertTrue(welcomeText.waitForExistence(timeout: 10), "Should be on login screen after logout") // Phase 5: Login again to verify re-login works UITestHelpers.login(app: app, username: testUser, password: testPassword) XCTAssertTrue(tabBar.waitForExistence(timeout: 10), "Should be logged in after re-login") // Phase 6: 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 // Already logged in via setUp — verify tab bar exists XCTAssertTrue(app.tabBars.firstMatch.waitForExistence(timeout: defaultTimeout), "Should be logged in") navigateToTab("Residences") let residenceName = "E2E Test Home \(testRunId)" // Phase 1: Create residence let addButton = app.buttons[AccessibilityIdentifiers.Residence.addButton].firstMatch XCTAssertTrue(addButton.waitForExistence(timeout: defaultTimeout), "Add residence button should exist") addButton.tap() // Fill form let nameField = app.textFields[AccessibilityIdentifiers.Residence.nameField] XCTAssertTrue(nameField.waitForExistence(timeout: 5), "Name field should exist") nameField.focusAndType(residenceName, app: app) // Use return key to move to next field or dismiss, then scroll dismissKeyboard() // Scroll to show more fields app.swipeUp() // Fill street field let streetField = app.textFields[AccessibilityIdentifiers.Residence.streetAddressField] if streetField.waitForExistence(timeout: 3) && streetField.isHittable { streetField.focusAndType("123 E2E Test St", app: app) dismissKeyboard() } // Fill city field let cityField = app.textFields[AccessibilityIdentifiers.Residence.cityField] if cityField.waitForExistence(timeout: 3) && cityField.isHittable { cityField.focusAndType("Austin", app: app) dismissKeyboard() } // Fill state field let stateField = app.textFields[AccessibilityIdentifiers.Residence.stateProvinceField] if stateField.waitForExistence(timeout: 3) && stateField.isHittable { stateField.focusAndType("TX", app: app) dismissKeyboard() } // Fill postal code field let postalField = app.textFields[AccessibilityIdentifiers.Residence.postalCodeField] if postalField.waitForExistence(timeout: 3) && postalField.isHittable { postalField.focusAndType("78701", app: app) } // Dismiss keyboard and scroll to save button dismissKeyboard() _ = app.keyboards.firstMatch.waitForNonExistence(timeout: defaultTimeout) app.swipeUp() // 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[AccessibilityIdentifiers.Residence.saveButton].firstMatch XCTAssertTrue(saveByLabel.waitForExistence(timeout: 5), "Save button should exist") saveByLabel.tap() } // Phase 2: Verify residence was created navigateToTab("Residences") 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 // Already logged in via setUp — verify tab bar exists XCTAssertTrue(app.tabBars.firstMatch.waitForExistence(timeout: defaultTimeout), "Should be logged in") // Ensure residence exists (precondition for task creation) if let residences = TestAccountAPIClient.listResidences(token: apiUser.token), residences.isEmpty { TestDataSeeder.createResidence(token: apiUser.token, name: "Task Test Home \(testRunId)") } navigateToResidences() pullToRefresh() // Navigate to Tasks navigateToTab("Tasks") let taskTitle = "E2E Task Lifecycle \(testRunId)" // 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() // Fill task form let titleField = app.textFields[AccessibilityIdentifiers.Task.titleField].firstMatch XCTAssertTrue(titleField.waitForExistence(timeout: 5), "Task title field should exist") titleField.focusAndType(taskTitle, app: app) dismissKeyboard() _ = app.keyboards.firstMatch.waitForNonExistence(timeout: defaultTimeout) app.swipeUp() // 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[AccessibilityIdentifiers.Task.saveButton].firstMatch XCTAssertTrue(saveByLabel.exists, "Save/Create button should exist") saveByLabel.tap() } // Phase 2: Verify task was created navigateToTab("Tasks") 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() { // Already logged in via setUp — verify tab bar exists XCTAssertTrue(app.tabBars.firstMatch.waitForExistence(timeout: defaultTimeout), "Should be logged in") navigateToTab("Tasks") // 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() { // Already logged in via setUp — verify tab bar exists XCTAssertTrue(app.tabBars.firstMatch.waitForExistence(timeout: defaultTimeout), "Should be logged in") // Verify user can access their residences tab navigateToTab("Residences") 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") 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() { // Already logged in via setUp — verify tab bar exists XCTAssertTrue(app.tabBars.firstMatch.waitForExistence(timeout: defaultTimeout), "Should be logged in") // Navigate to add residence to check residence types are loaded navigateToTab("Residences") let addButton = app.buttons[AccessibilityIdentifiers.Residence.addButton].firstMatch addButton.waitForExistenceOrFail(timeout: navigationTimeout, message: "Add residence button should exist") addButton.tap() // Check property type picker exists (indicates lookups loaded) let propertyTypePicker = app.buttons[AccessibilityIdentifiers.Residence.propertyTypePicker].firstMatch XCTAssertTrue(propertyTypePicker.waitForExistence(timeout: navigationTimeout), "Property type picker should exist (lookups loaded)") // Cancel form let cancelButton = app.buttons[AccessibilityIdentifiers.Residence.formCancelButton].firstMatch if cancelButton.exists { cancelButton.tap() } } // MARK: - Test 7: Residence Sharing Flow // Mirrors TestIntegration_ResidenceSharingFlow func test07_residenceSharingUIElements() { // Already logged in via setUp — verify tab bar exists XCTAssertTrue(app.tabBars.firstMatch.waitForExistence(timeout: defaultTimeout), "Should be logged in") navigateToTab("Residences") // Find any residence to check sharing UI let residenceCard = app.cells.firstMatch if residenceCard.waitForExistence(timeout: defaultTimeout) { residenceCard.tap() // 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() } } } // MARK: - Helper: Create Minimal Residence private func createMinimalResidence(name: String) { let addButton = app.buttons[AccessibilityIdentifiers.Residence.addButton].firstMatch guard addButton.waitForExistence(timeout: 5) else { return } addButton.tap() // Fill name field let nameField = app.textFields[AccessibilityIdentifiers.Residence.nameField] if nameField.waitForExistence(timeout: defaultTimeout) { nameField.focusAndType(name, app: app) dismissKeyboard() } // Scroll to show address fields app.swipeUp() // Fill street field let streetField = app.textFields[AccessibilityIdentifiers.Residence.streetAddressField] if streetField.waitForExistence(timeout: 3) && streetField.isHittable { streetField.focusAndType("123 Test St", app: app) dismissKeyboard() } // Fill city field let cityField = app.textFields[AccessibilityIdentifiers.Residence.cityField] if cityField.waitForExistence(timeout: 3) && cityField.isHittable { cityField.focusAndType("Austin", app: app) dismissKeyboard() } // Fill state field let stateField = app.textFields[AccessibilityIdentifiers.Residence.stateProvinceField] if stateField.waitForExistence(timeout: 3) && stateField.isHittable { stateField.focusAndType("TX", app: app) dismissKeyboard() } // Fill postal code field let postalField = app.textFields[AccessibilityIdentifiers.Residence.postalCodeField] if postalField.waitForExistence(timeout: 3) && postalField.isHittable { postalField.focusAndType("78701", app: app) } dismissKeyboard() _ = app.keyboards.firstMatch.waitForNonExistence(timeout: defaultTimeout) app.swipeUp() // Save let saveButton = app.buttons[AccessibilityIdentifiers.Residence.saveButton].firstMatch if saveButton.waitForExistence(timeout: 5) && saveButton.isHittable { saveButton.tap() } // Wait for save to complete and return to list _ = app.cells.firstMatch.waitForExistence(timeout: defaultTimeout) } // MARK: - Helper: Find Add Task Button private func findAddTaskButton() -> XCUIElement { let addButton = app.buttons[AccessibilityIdentifiers.Task.addButton].firstMatch if addButton.exists { return addButton } return app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Add Task' OR label CONTAINS[c] 'New Task'")).firstMatch } }