Files
honeyDueKMP/iosApp/HoneyDueUITests/Suite5_TaskTests.swift
treyt 5c360a2796 Rearchitect UI test suite for complete, non-flaky coverage against live API
- Migrate Suite4-10, SmokeTests, NavigationCriticalPathTests to AuthenticatedTestCase
  with seeded admin account and real backend login
- Add 34 accessibility identifiers across 11 app views (task completion, profile,
  notifications, theme, join residence, manage users, forms)
- Create FeatureCoverageTests (14 tests) covering previously untested features:
  profile edit, theme selection, notification prefs, task completion, manage users,
  join residence, task templates
- Create MultiUserSharingTests (18 API tests) and MultiUserSharingUITests (8 XCUI
  tests) for full cross-user residence sharing lifecycle
- Add cleanup infrastructure: SuiteZZ_CleanupTests auto-wipes test data after runs,
  cleanup_test_data.sh script for manual reset via admin API
- Add share code API methods to TestAccountAPIClient (generateShareCode, joinWithCode,
  getShareCode, listResidenceUsers, removeUser)
- Fix app bugs found by tests:
  - ResidencesListView join callback now uses forceRefresh:true
  - APILayer invalidates task cache when residence count changes
  - AllTasksView auto-reloads tasks when residence list changes
- Fix test quality: keyboard focus waits, Save/Add button label matching,
  Documents tab label (Docs), remove API verification from UI tests
- DataLayerTests and PasswordResetTests now verify through UI, not API calls

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

290 lines
11 KiB
Swift

import XCTest
/// Task management tests
/// Uses UITestHelpers for consistent login/logout behavior
/// IMPORTANT: Tasks require at least one residence to exist
///
/// 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
final class Suite5_TaskTests: AuthenticatedTestCase {
override var useSeededAccount: Bool { true }
override func setUpWithError() throws {
try super.setUpWithError()
navigateToTasks()
}
override func tearDownWithError() throws {
try super.tearDownWithError()
}
// MARK: - Helper Methods
/// Finds the Add Task button using multiple strategies
/// The button exists in two places:
/// 1. Toolbar (always visible when residences exist)
/// 2. Empty state (visible when no tasks exist)
private func findAddTaskButton() -> XCUIElement {
sleep(2) // Wait for screen to fully render
// Strategy 1: Try accessibility identifier
let addButtonById = app.buttons[AccessibilityIdentifiers.Task.addButton].firstMatch
if addButtonById.exists && addButtonById.isEnabled {
return addButtonById
}
// Strategy 2: Look for toolbar add button (navigation bar plus button)
let navBarButtons = app.navigationBars.buttons
for i in 0..<navBarButtons.count {
let button = navBarButtons.element(boundBy: i)
if button.label == "plus" || button.label.contains("Add") {
if button.isEnabled {
return button
}
}
}
// Strategy 3: Try finding "Add Task" button in empty state by text
let emptyStateButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Add Task'")).firstMatch
if emptyStateButton.exists && emptyStateButton.isEnabled {
return emptyStateButton
}
// Strategy 4: Look for any enabled button with a plus icon
let allButtons = app.buttons
for i in 0..<min(allButtons.count, 20) { // Check first 20 buttons
let button = allButtons.element(boundBy: i)
if button.isEnabled && (button.label.contains("plus") || button.label.contains("Add")) {
return button
}
}
// Return the identifier one as fallback (will fail assertion if doesn't exist)
return addButtonById
}
// MARK: - 1. Error/Validation Tests
func test01_cancelTaskCreation() {
// Given: User is on add task form
navigateToTasks()
sleep(3)
let addButton = findAddTaskButton()
XCTAssertTrue(addButton.exists && addButton.isEnabled, "Add task button should exist and be enabled")
addButton.tap()
sleep(3)
// Verify form opened
let titleField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Title'")).firstMatch
XCTAssertTrue(titleField.waitForExistence(timeout: 5), "Task form should open")
// When: User taps cancel
let cancelButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Cancel'")).firstMatch
XCTAssertTrue(cancelButton.exists, "Cancel button should exist in task form")
cancelButton.tap()
sleep(2)
// Then: Should return to tasks list
let tasksTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch
XCTAssertTrue(tasksTab.exists, "Should be back on tasks list after cancel")
}
// MARK: - 2. View/List Tests
func test02_tasksTabExists() {
// Given: User is logged in
// When: User looks for Tasks tab
let tasksTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch
// Then: Tasks tab should exist
XCTAssertTrue(tasksTab.exists, "Tasks tab should exist in main tab bar")
XCTAssertTrue(tasksTab.isSelected, "Tasks tab should be selected after navigation")
}
func test03_viewTasksList() {
// Given: User is on Tasks tab
navigateToTasks()
sleep(3)
// Then: Tasks screen should be visible
// Verify we're on the right screen by checking for the navigation title
let tasksTitle = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'All Tasks' OR label CONTAINS[c] 'Tasks'")).firstMatch
XCTAssertTrue(tasksTitle.waitForExistence(timeout: 5), "Tasks screen title should be visible")
}
func test04_addTaskButtonExists() {
// Given: User is on Tasks tab with at least one residence
navigateToTasks()
sleep(3)
// Then: Add task button should exist and be enabled
let addButton = findAddTaskButton()
XCTAssertTrue(addButton.exists, "Add task button should exist on Tasks screen")
XCTAssertTrue(addButton.isEnabled, "Add task button should be enabled when residence exists")
}
func test05_navigateToAddTask() {
// Given: User is on Tasks tab
navigateToTasks()
sleep(3)
// When: User taps add task button
let addButton = findAddTaskButton()
XCTAssertTrue(addButton.exists, "Add task button should exist")
XCTAssertTrue(addButton.isEnabled, "Add task button should be enabled")
addButton.tap()
sleep(3)
// Then: Should show add task form with required fields
let titleField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Title' OR placeholderValue CONTAINS[c] 'Task'")).firstMatch
XCTAssertTrue(titleField.waitForExistence(timeout: 5), "Task title field should appear in add form")
let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save' OR label CONTAINS[c] 'Add'")).firstMatch
XCTAssertTrue(saveButton.exists, "Save/Add button should exist in add task form")
}
// MARK: - 3. Creation Tests
func test06_createBasicTask() {
// Given: User is on Tasks tab
navigateToTasks()
sleep(3)
// When: User taps add task button
let addButton = findAddTaskButton()
XCTAssertTrue(addButton.exists && addButton.isEnabled, "Add task button should exist and be enabled")
addButton.tap()
sleep(3)
// Verify task form loaded
let titleField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Title'")).firstMatch
XCTAssertTrue(titleField.waitForExistence(timeout: 5), "Task title field should appear")
// Fill in task title with unique timestamp
let timestamp = Int(Date().timeIntervalSince1970)
let taskTitle = "UITest Task \(timestamp)"
titleField.tap()
titleField.typeText(taskTitle)
// Scroll down to find and fill description
app.swipeUp()
sleep(1)
let descField = app.textViews.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Description'")).firstMatch
if descField.exists {
descField.tap()
descField.typeText("Test task")
}
// Scroll to find Save button
app.swipeUp()
sleep(1)
// When: User taps save/add
let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save' OR label CONTAINS[c] 'Add'")).firstMatch
XCTAssertTrue(saveButton.exists, "Save/Add button should exist")
saveButton.tap()
// Then: Should return to tasks list
sleep(5) // Wait for API call to complete
// Verify we're back on tasks list by checking tab exists
let tasksTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch
XCTAssertTrue(tasksTab.exists, "Should be back on tasks list after saving")
// Verify task appears in the list (may be in kanban columns)
let newTask = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(taskTitle)'")).firstMatch
XCTAssertTrue(newTask.waitForExistence(timeout: 10), "New task '\(taskTitle)' should appear in the list")
}
// MARK: - 4. View Details Tests
func test07_viewTaskDetails() {
// Given: User is on Tasks tab and at least one task exists
navigateToTasks()
sleep(3)
// Look for any task in the list
let taskCard = app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'UITest Task' OR label CONTAINS 'Test'")).firstMatch
if !taskCard.waitForExistence(timeout: 5) {
// No task found - skip this test
print("No tasks found - run testCreateBasicTask first")
return
}
// When: User taps on a task
taskCard.tap()
sleep(2)
// Then: Should show task details screen
let editButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Edit'")).firstMatch
let completeButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Complete' OR label CONTAINS[c] 'Mark'")).firstMatch
let backButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Back' OR label CONTAINS[c] 'Tasks'")).firstMatch
let detailScreenVisible = editButton.exists || completeButton.exists || backButton.exists
XCTAssertTrue(detailScreenVisible, "Task details screen should show with action buttons")
}
// MARK: - 5. Navigation Tests
func test08_navigateToContractors() {
// Given: User is on Tasks tab
navigateToTasks()
sleep(1)
// When: User taps Contractors tab
let contractorsTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Contractors'")).firstMatch
XCTAssertTrue(contractorsTab.waitForExistence(timeout: 5), "Contractors tab should exist")
contractorsTab.tap()
sleep(1)
// Then: Should be on Contractors tab
XCTAssertTrue(contractorsTab.isSelected, "Contractors tab should be selected")
}
func test09_navigateToDocuments() {
// Given: User is on Tasks tab
navigateToTasks()
sleep(1)
// When: User taps Documents tab
let documentsTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Doc'")).firstMatch
XCTAssertTrue(documentsTab.waitForExistence(timeout: 5), "Documents tab should exist")
documentsTab.tap()
sleep(1)
// Then: Should be on Documents tab
XCTAssertTrue(documentsTab.isSelected, "Documents tab should be selected")
}
func test10_navigateBetweenTabs() {
// Given: User is on Tasks tab
navigateToTasks()
sleep(1)
// When: User navigates to Residences tab
let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
XCTAssertTrue(residencesTab.exists, "Residences tab should exist")
residencesTab.tap()
sleep(1)
// Then: Should be on Residences tab
XCTAssertTrue(residencesTab.isSelected, "Should be on Residences tab")
// When: User navigates back to Tasks
let tasksTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch
tasksTab.tap()
sleep(2)
// Then: Should be back on Tasks tab
XCTAssertTrue(tasksTab.isSelected, "Should be back on Tasks tab")
}
}