Files
honeyDueKMP/iosApp/HoneyDueUITests/Suite6_ComprehensiveTaskTests.swift
Trey T 4df8707b92 UI test infrastructure overhaul — 58% to 96% pass rate (231/241)
Major infrastructure changes:
- BaseUITestCase: per-suite app termination via class setUp() prevents
  stale state when parallel clones share simulators
- relaunchBetweenTests override for suites that modify login/onboarding state
- focusAndType: dedicated SecureTextField path handles iOS strong password
  autofill suggestions (Choose My Own Password / Not Now dialogs)
- LoginScreenObject: tapSignUp/tapForgotPassword use scrollIntoView for
  offscreen buttons instead of simple swipeUp
- Removed all coordinate taps from ForgotPasswordScreen, VerifyResetCodeScreen,
  ResetPasswordScreen (Rule 3 compliance)
- Removed all usleep calls from screen objects (Rule 14 compliance)

App fixes exposed by tests:
- ContractorsListView: added onDismiss to sheet for list refresh after save
- AllTasksView: added Task.RefreshButton accessibility identifier
- AccessibilityIdentifiers: added Task.refreshButton
- DocumentsWarrantiesView: onDismiss handler for document list refresh
- Various form views: textContentType, submitLabel, onSubmit for keyboard flow

Test fixes:
- PasswordResetTests: handle auto-login after reset (app skips success screen)
- AuthenticatedUITestCase: refreshTasks() helper for kanban toolbar button
- All pre-login suites use relaunchBetweenTests for test independence
- Deleted dead code: AuthenticatedTestCase, SeededTestData, SeedTests,
  CleanupTests, old Suite0/2/3, Suite1_RegistrationRebuildTests

10 remaining failures: 5 iOS strong password autofill (simulator env),
3 pull-to-refresh gesture on empty lists, 2 feature coverage edge cases.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 15:05:37 -05:00

371 lines
14 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] = []
override func setUpWithError() throws {
try super.setUpWithError()
// 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)
}
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")
task.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")
}
}