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>
371 lines
14 KiB
Swift
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")
|
|
}
|
|
|
|
|
|
}
|