import XCTest /// Integration tests for task operations against the real local backend. /// /// Test Plan IDs: TASK-010, TASK-012, plus create/edit flows. /// Data is seeded via API and cleaned up in tearDown. final class TaskIntegrationTests: AuthenticatedTestCase { override var useSeededAccount: Bool { true } // MARK: - Create Task func testTASK_CreateTaskAppearsInList() { // Seed a residence via API so task creation has a valid target let residence = cleaner.seedResidence() navigateToTasks() let addButton = app.buttons[AccessibilityIdentifiers.Task.addButton] let emptyState = app.otherElements[AccessibilityIdentifiers.Task.emptyStateView] let taskList = app.otherElements[AccessibilityIdentifiers.Task.tasksList] let loaded = addButton.waitForExistence(timeout: defaultTimeout) || emptyState.waitForExistence(timeout: 3) || taskList.waitForExistence(timeout: 3) XCTAssertTrue(loaded, "Tasks screen should load") if addButton.exists && addButton.isHittable { addButton.forceTap() } else { let emptyAddButton = app.buttons.containing( NSPredicate(format: "label CONTAINS[c] 'Add' OR label CONTAINS[c] 'Create'") ).firstMatch emptyAddButton.waitForExistenceOrFail(timeout: defaultTimeout) emptyAddButton.forceTap() } let titleField = app.textFields[AccessibilityIdentifiers.Task.titleField] titleField.waitForExistenceOrFail(timeout: defaultTimeout) let uniqueTitle = "IntTest Task \(Int(Date().timeIntervalSince1970))" titleField.forceTap() titleField.typeText(uniqueTitle) let saveButton = app.buttons[AccessibilityIdentifiers.Task.saveButton] saveButton.scrollIntoView(in: app.scrollViews.firstMatch) saveButton.forceTap() let newTask = app.staticTexts[uniqueTitle] XCTAssertTrue( newTask.waitForExistence(timeout: longTimeout), "Newly created task should appear" ) } // MARK: - TASK-010: Uncancel Task func testTASK010_UncancelTaskFlow() throws { // Seed a cancelled task via API let residence = cleaner.seedResidence() let cancelledTask = TestDataSeeder.createCancelledTask(token: session.token, residenceId: residence.id) cleaner.trackTask(cancelledTask.id) navigateToTasks() // Find the cancelled task let taskText = app.staticTexts[cancelledTask.title] guard taskText.waitForExistence(timeout: defaultTimeout) else { throw XCTSkip("Cancelled task not visible in current view") } taskText.forceTap() // Look for an uncancel or reopen button let uncancelButton = app.buttons.containing( NSPredicate(format: "label CONTAINS[c] 'Uncancel' OR label CONTAINS[c] 'Reopen' OR label CONTAINS[c] 'Restore'") ).firstMatch if uncancelButton.waitForExistence(timeout: defaultTimeout) { uncancelButton.forceTap() let statusText = app.staticTexts.containing( NSPredicate(format: "label CONTAINS[c] 'Cancelled'") ).firstMatch XCTAssertFalse(statusText.exists, "Task should no longer show as cancelled after uncancel") } } // MARK: - TASK-010 (v2): Uncancel Task — Restores Cancelled Task to Active Lifecycle func test15_uncancelRestorescancelledTask() throws { // Seed a residence and a task, then cancel the task via API let residence = cleaner.seedResidence(name: "Uncancel Test Residence \(Int(Date().timeIntervalSince1970))") let task = cleaner.seedTask(residenceId: residence.id, title: "Uncancel Me \(Int(Date().timeIntervalSince1970))") guard TestAccountAPIClient.cancelTask(token: session.token, id: task.id) != nil else { throw XCTSkip("Could not cancel task via API — skipping uncancel test") } navigateToTasks() // The cancelled task should be visible somewhere on the tasks screen // (e.g., in a Cancelled column or section) let taskText = app.staticTexts[task.title] guard taskText.waitForExistence(timeout: longTimeout) else { throw XCTSkip("Cancelled task '\(task.title)' not visible — may require a Cancelled filter to be active") } taskText.forceTap() // Look for an uncancel / reopen / restore action let uncancelButton = app.buttons.containing( NSPredicate(format: "label CONTAINS[c] 'Uncancel' OR label CONTAINS[c] 'Reopen' OR label CONTAINS[c] 'Restore'") ).firstMatch guard uncancelButton.waitForExistence(timeout: defaultTimeout) else { throw XCTSkip("No uncancel button found — feature may not yet be implemented in UI") } uncancelButton.forceTap() // After uncancelling, the task should no longer show a Cancelled status label let cancelledLabel = app.staticTexts.containing( NSPredicate(format: "label CONTAINS[c] 'Cancelled'") ).firstMatch XCTAssertFalse( cancelledLabel.waitForExistence(timeout: defaultTimeout), "Task should no longer display 'Cancelled' status after being restored" ) } // MARK: - TASK-004: Create Task from Template func test16_createTaskFromTemplate() throws { // Seed a residence so template-created tasks have a valid target cleaner.seedResidence(name: "Template Test Residence \(Int(Date().timeIntervalSince1970))") navigateToTasks() // Tap the add task button (or empty-state equivalent) let addButton = app.buttons[AccessibilityIdentifiers.Task.addButton] let emptyAddButton = app.buttons.containing( NSPredicate(format: "label CONTAINS[c] 'Add' OR label CONTAINS[c] 'Create'") ).firstMatch let addVisible = addButton.waitForExistence(timeout: defaultTimeout) || emptyAddButton.waitForExistence(timeout: 3) XCTAssertTrue(addVisible, "An add/create task button should be visible on the tasks screen") if addButton.exists && addButton.isHittable { addButton.forceTap() } else { emptyAddButton.forceTap() } // Look for a Templates or Browse Templates option within the add-task flow. // NOTE: The exact accessibility identifier for the template browser is not yet defined // in AccessibilityIdentifiers.swift. The identifiers below use the pattern established // in the codebase (e.g., "TaskForm.TemplatesButton") and will need to be wired up in // the SwiftUI view when the template browser feature is implemented. let templateButton = app.buttons.containing( NSPredicate(format: "label CONTAINS[c] 'Template' OR label CONTAINS[c] 'Browse'") ).firstMatch guard templateButton.waitForExistence(timeout: defaultTimeout) else { throw XCTSkip("Template browser not yet reachable from the add-task flow — skipping") } templateButton.forceTap() // Select the first available template let firstTemplate = app.cells.firstMatch guard firstTemplate.waitForExistence(timeout: defaultTimeout) else { throw XCTSkip("No templates available in template browser — skipping") } firstTemplate.forceTap() // After selecting a template the form should be pre-filled — the title field should // contain something (i.e., not be empty) let titleField = app.textFields[AccessibilityIdentifiers.Task.titleField] titleField.waitForExistenceOrFail(timeout: defaultTimeout) let preFilledTitle = titleField.value as? String ?? "" XCTAssertFalse( preFilledTitle.isEmpty, "Title field should be pre-filled by the selected template" ) // Save the templated task let saveButton = app.buttons[AccessibilityIdentifiers.Task.saveButton] saveButton.scrollIntoView(in: app.scrollViews.firstMatch) saveButton.forceTap() // The task should now appear in the list let savedTask = app.staticTexts[preFilledTitle] XCTAssertTrue( savedTask.waitForExistence(timeout: longTimeout), "Task created from template ('\(preFilledTitle)') should appear in the task list" ) } // MARK: - TASK-012: Delete Task func testTASK012_DeleteTaskUpdatesViews() { // Seed a task via API let residence = cleaner.seedResidence() let task = cleaner.seedTask(residenceId: residence.id, title: "Delete Task \(Int(Date().timeIntervalSince1970))") navigateToTasks() // Find and open the task let taskText = app.staticTexts[task.title] taskText.waitForExistenceOrFail(timeout: longTimeout) taskText.forceTap() // Delete the task let deleteButton = app.buttons[AccessibilityIdentifiers.Task.deleteButton] deleteButton.waitForExistenceOrFail(timeout: defaultTimeout) deleteButton.forceTap() // Confirm deletion let confirmDelete = app.alerts.buttons.containing( NSPredicate(format: "label CONTAINS[c] 'Delete' OR label CONTAINS[c] 'Confirm'") ).firstMatch let alertConfirmButton = app.buttons[AccessibilityIdentifiers.Alert.confirmButton] if alertConfirmButton.waitForExistence(timeout: shortTimeout) { alertConfirmButton.tap() } else if confirmDelete.waitForExistence(timeout: shortTimeout) { confirmDelete.tap() } let deletedTask = app.staticTexts[task.title] XCTAssertTrue( deletedTask.waitForNonExistence(timeout: longTimeout), "Deleted task should no longer appear in views" ) } }