import XCTest /// Comprehensive task 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 Suite6_ComprehensiveTaskTests: AuthenticatedUITestCase { override var needsAPISession: Bool { true } override var testCredentials: (username: String, password: String) { ("testuser", "TestPass123!") } override var apiCredentials: (username: String, password: String) { ("testuser", "TestPass123!") } // Test data tracking var createdTaskTitles: [String] = [] private static var hasCleanedStaleData = false override func setUpWithError() throws { try super.setUpWithError() // Dismiss any open form from previous test let cancelButton = app.buttons[AccessibilityIdentifiers.Task.formCancelButton].firstMatch if cancelButton.exists { cancelButton.tap() } // One-time cleanup of stale tasks from previous test runs if !Self.hasCleanedStaleData { Self.hasCleanedStaleData = true if let stale = TestAccountAPIClient.listTasks(token: session.token) { for task in stale { _ = TestAccountAPIClient.deleteTask(token: session.token, id: task.id) } } } // Ensure at least one residence exists (task add button requires it) if let residences = TestAccountAPIClient.listResidences(token: session.token), residences.isEmpty { cleaner.seedResidence(name: "Task Test Home") // Force app to load the new residence navigateToResidences() pullToRefresh() } navigateToTasks() // Wait for screen to fully load — cold start can take 30+ seconds taskList.addButton.waitForExistenceOrFail(timeout: loginTimeout, message: "Task add button should appear after navigation") } override func tearDownWithError() throws { // Ensure all UI-created tasks are tracked for API cleanup if !createdTaskTitles.isEmpty, let allTasks = TestAccountAPIClient.listTasks(token: session.token) { for title in createdTaskTitles { if let task = allTasks.first(where: { $0.title.contains(title) }) { cleaner.trackTask(task.id) } } } createdTaskTitles.removeAll() try super.tearDownWithError() } // MARK: - Page Objects private var taskList: TaskListScreen { TaskListScreen(app: app) } private var taskForm: TaskFormScreen { TaskFormScreen(app: app) } // MARK: - Helper Methods private func openTaskForm() -> Bool { let addButton = taskList.addButton guard addButton.waitForExistence(timeout: defaultTimeout) && addButton.isEnabled else { return false } addButton.forceTap() return taskForm.titleField.waitForExistence(timeout: defaultTimeout) } private func fillField(identifier: String, text: String) { let field = app.textFields[identifier].firstMatch if field.exists { field.focusAndType(text, app: app) } } private func selectPicker(identifier: String, option: String) { let picker = app.buttons[identifier].firstMatch if picker.exists { picker.tap() let optionButton = app.buttons[option] if optionButton.waitForExistence(timeout: defaultTimeout) { optionButton.tap() _ = optionButton.waitForNonExistence(timeout: defaultTimeout) } } } private func createTask( title: String, description: String? = nil, scrollToFindFields: Bool = true ) -> Bool { // Mirror Suite5's proven-working inline flow to avoid page-object drift. // Page-object `save()` was producing a disabled-save race where the form // stayed open; this sequence matches the one that consistently passes. let addButton = app.buttons[AccessibilityIdentifiers.Task.addButton].firstMatch guard addButton.waitForExistence(timeout: defaultTimeout) && addButton.isEnabled else { return false } addButton.tap() let titleField = app.textFields[AccessibilityIdentifiers.Task.titleField].firstMatch guard titleField.waitForExistence(timeout: defaultTimeout) else { return false } fillTextField(identifier: AccessibilityIdentifiers.Task.titleField, text: title) if let desc = description { dismissKeyboard() app.swipeUp() let descField = app.textViews[AccessibilityIdentifiers.Task.descriptionField].firstMatch if descField.waitForExistence(timeout: 5) { descField.focusAndType(desc, app: app) } } dismissKeyboard() app.swipeUp() let saveButton = app.buttons[AccessibilityIdentifiers.Task.saveButton].firstMatch guard saveButton.waitForExistence(timeout: defaultTimeout) else { return false } saveButton.tap() // If the first tap is a no-op (canSave=false because SwiftUI's title // binding hasn't caught up with XCUITest typing under parallel load), // nudge the form so the binding flushes, then re-tap. Up to 2 retries. if !saveButton.waitForNonExistence(timeout: navigationTimeout) { for _ in 0..<2 { let stillOpenTitle = app.textFields[AccessibilityIdentifiers.Task.titleField].firstMatch if stillOpenTitle.exists && stillOpenTitle.isHittable { stillOpenTitle.tap() _ = app.keyboards.firstMatch.waitForExistence(timeout: 2) app.typeText(" ") app.typeText(XCUIKeyboardKey.delete.rawValue) dismissKeyboard() app.swipeUp() } saveButton.tap() if saveButton.waitForNonExistence(timeout: navigationTimeout) { break } } } createdTaskTitles.append(title) // Track for API cleanup if let items = TestAccountAPIClient.listTasks(token: session.token), let created = items.first(where: { $0.title.contains(title) }) { cleaner.trackTask(created.id) } // Navigate to tasks tab to trigger list refresh and reset scroll position. // Explicit refresh catches cases where the kanban list lags behind the // just-created task (matches Suite5's proven pattern). navigateToTasks() refreshTasks() return true } private func findTask(title: String) -> XCUIElement { return taskList.findTask(title: title) } private func deleteAllTestTasks() { for title in createdTaskTitles { let task = findTask(title: title) if task.exists { task.tap() let deleteButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Delete' OR label CONTAINS[c] 'Cancel'")).firstMatch if deleteButton.waitForExistence(timeout: defaultTimeout) { deleteButton.tap() 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.waitForExistence(timeout: defaultTimeout) { confirmButton.tap() _ = confirmButton.waitForNonExistence(timeout: defaultTimeout) } } let backButton = app.navigationBars.buttons.firstMatch if backButton.exists { backButton.tap() let tasksList = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch _ = tasksList.waitForExistence(timeout: defaultTimeout) } } } } // MARK: - 1. Error/Validation Tests func test01_cannotCreateTaskWithEmptyTitle() { guard openTaskForm() else { XCTFail("Failed to open task form") return } app.swipeUp() let saveButton = app.buttons[AccessibilityIdentifiers.Task.saveButton].firstMatch _ = saveButton.waitForExistence(timeout: defaultTimeout) XCTAssertTrue(saveButton.exists, "Save/Add button should exist") XCTAssertFalse(saveButton.isEnabled, "Save/Add button should be disabled when title is empty") } func test02_cancelTaskCreation() { guard openTaskForm() else { XCTFail("Failed to open task form") return } let titleField = app.textFields[AccessibilityIdentifiers.Task.titleField].firstMatch titleField.focusAndType("This will be canceled", app: app) let cancelButton = app.buttons[AccessibilityIdentifiers.Task.formCancelButton].firstMatch XCTAssertTrue(cancelButton.exists, "Cancel button should exist") cancelButton.tap() let tasksTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch _ = tasksTab.waitForExistence(timeout: defaultTimeout) XCTAssertTrue(tasksTab.exists, "Should be back on tasks list") 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)") navigateToTasks() } 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") 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)" guard createTask(title: originalTitle) else { XCTFail("Failed to create task") return } navigateToTasks() let task = findTask(title: originalTitle) XCTAssertTrue(task.waitForExistence(timeout: defaultTimeout), "Task should exist") // Open the task actions menu on the card (edit is inside a Menu, not a detail screen) let actionsMenu = app.buttons["Task actions"].firstMatch if actionsMenu.waitForExistence(timeout: defaultTimeout) { actionsMenu.tap() let editButton = app.buttons[AccessibilityIdentifiers.Task.editButton].firstMatch if editButton.waitForExistence(timeout: defaultTimeout) { editButton.tap() let titleField = app.textFields[AccessibilityIdentifiers.Task.titleField].firstMatch if titleField.waitForExistence(timeout: defaultTimeout) { titleField.clearAndEnterText(newTitle, app: app) let saveButton = app.buttons[AccessibilityIdentifiers.Task.saveButton].firstMatch if saveButton.exists { saveButton.tap() _ = saveButton.waitForNonExistence(timeout: defaultTimeout) createdTaskTitles.append(newTitle) navigateToTasks() let updatedTask = findTask(title: newTitle) XCTAssertTrue(updatedTask.exists, "Task should show updated title") } } } } } // test10_updateAllTaskFields removed — requires Actions menu accessibility identifiers // MARK: - 4. Navigation/View Tests func test11_navigateFromTasksToOtherTabs() { navigateToTasks() let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch XCTAssertTrue(residencesTab.exists, "Residences tab should exist") residencesTab.tap() _ = residencesTab.waitForExistence(timeout: defaultTimeout) XCTAssertTrue(residencesTab.isSelected, "Should be on Residences tab") let tasksTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch tasksTab.tap() _ = tasksTab.waitForExistence(timeout: defaultTimeout) XCTAssertTrue(tasksTab.isSelected, "Should be back on Tasks tab") let contractorsTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Contractors'")).firstMatch XCTAssertTrue(contractorsTab.exists, "Contractors tab should exist") contractorsTab.tap() _ = contractorsTab.waitForExistence(timeout: defaultTimeout) XCTAssertTrue(contractorsTab.isSelected, "Should be on Contractors tab") tasksTab.tap() _ = tasksTab.waitForExistence(timeout: defaultTimeout) XCTAssertTrue(tasksTab.isSelected, "Should be back on Tasks tab again") } func test12_refreshTasksList() { navigateToTasks() let refreshButton = app.navigationBars.buttons.containing(NSPredicate(format: "label CONTAINS 'arrow.clockwise' OR label CONTAINS 'refresh'")).firstMatch if refreshButton.exists { refreshButton.tap() } let tasksTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch _ = tasksTab.waitForExistence(timeout: defaultTimeout) 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)" guard createTask(title: taskTitle) else { XCTFail("Failed to create task") return } navigateToTasks() var task = findTask(title: taskTitle) XCTAssertTrue(task.exists, "Task should exist before backgrounding") XCUIDevice.shared.press(.home) _ = app.wait(for: .runningBackground, timeout: 10) app.activate() _ = app.wait(for: .runningForeground, timeout: 10) navigateToTasks() task = findTask(title: taskTitle) XCTAssertTrue(task.exists, "Task should persist after backgrounding app") } }