- 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>
290 lines
11 KiB
Swift
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")
|
|
}
|
|
}
|