Files
honeyDueKMP/iosApp/HoneyDueUITests/Suite6_ComprehensiveTaskTests.swift
Trey T d545fd463c Fix 10 failing UI tests: kanban scroll, menu-based edit, form submit reliability
- Screens.swift: findTask() now scrolls through kanban columns (swipe left/right)
  to locate tasks rendered off-screen in LazyHGrid
- Suite5: test06/07 use refreshTasks() instead of pullToRefresh() (kanban is
  horizontal), add API call before navigate for server processing delay
- Suite6: test09 opens "Task actions" menu before tapping edit (no detail screen)
- Suite8: submitForm() uses coordinate-based keyboard dismiss, retry tap, and
  longer timeout; test22/23 re-navigate after creation and use waitForExistence

Test results: 141/143 passed (was 131/143). Remaining 2 failures are pre-existing
(Suite1 test11) and flaky/unrelated (Suite3 testR307).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 17:21:31 -05:00

395 lines
15 KiB
Swift

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 {
guard openTaskForm() else { return false }
taskForm.enterTitle(title)
if let desc = description {
taskForm.enterDescription(desc)
}
taskForm.save()
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
navigateToTasks()
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")
}
}