import XCTest /// Comprehensive residence 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 Suite4_ComprehensiveResidenceTests: AuthenticatedUITestCase { override var needsAPISession: Bool { true } // Test data tracking var createdResidenceNames: [String] = [] override func setUpWithError() throws { try super.setUpWithError() // Dismiss any open form/sheet from a previous test let cancelButton = app.buttons[AccessibilityIdentifiers.Residence.formCancelButton].firstMatch if cancelButton.exists { cancelButton.tap() } navigateToResidences() residenceList.addButton.waitForExistenceOrFail(timeout: navigationTimeout, message: "Residence add button should appear after navigation") } override func tearDownWithError() throws { // Ensure all UI-created residences are tracked for API cleanup if !createdResidenceNames.isEmpty, let allResidences = TestAccountAPIClient.listResidences(token: session.token) { for name in createdResidenceNames { if let res = allResidences.first(where: { $0.name.contains(name) }) { cleaner.trackResidence(res.id) } } } createdResidenceNames.removeAll() try super.tearDownWithError() } // MARK: - Page Objects private var residenceList: ResidenceListScreen { ResidenceListScreen(app: app) } private var residenceForm: ResidenceFormScreen { ResidenceFormScreen(app: app) } private var residenceDetail: ResidenceDetailScreen { ResidenceDetailScreen(app: app) } // MARK: - Helper Methods private func openResidenceForm(file: StaticString = #filePath, line: UInt = #line) { let addButton = residenceList.addButton addButton.waitForExistenceOrFail(timeout: defaultTimeout, message: "Residence add button should exist", file: file, line: line) XCTAssertTrue(addButton.isEnabled, "Residence add button should be enabled", file: file, line: line) addButton.tap() residenceForm.nameField.waitForExistenceOrFail(timeout: defaultTimeout, message: "Residence form should open", file: file, line: line) } /// Fill sequential address fields using the Return key to advance focus. /// Fill address fields. Dismisses keyboard between each field for clean focus. private func fillAddressFields(street: String, city: String, state: String, postal: String) { // Scroll address section into view — may need multiple swipes on smaller screens let streetField = app.textFields[AccessibilityIdentifiers.Residence.streetAddressField].firstMatch for _ in 0..<3 { if streetField.exists && streetField.isHittable { break } app.swipeUp() } streetField.waitForExistenceOrFail(timeout: navigationTimeout, message: "Street field should appear after scroll") fillTextField(identifier: AccessibilityIdentifiers.Residence.streetAddressField, text: street) dismissKeyboard() fillTextField(identifier: AccessibilityIdentifiers.Residence.cityField, text: city) dismissKeyboard() fillTextField(identifier: AccessibilityIdentifiers.Residence.stateProvinceField, text: state) dismissKeyboard() fillTextField(identifier: AccessibilityIdentifiers.Residence.postalCodeField, text: postal) dismissKeyboard() } private func selectPropertyType(type: String) { let picker = app.buttons[AccessibilityIdentifiers.Residence.propertyTypePicker].firstMatch guard picker.waitForExistence(timeout: defaultTimeout) else { XCTFail("Property type picker not found") return } picker.tap() // SwiftUI Picker in Form pushes a selection list — find the option by text let option = app.staticTexts[type] option.waitForExistenceOrFail(timeout: navigationTimeout, message: "Property type '\(type)' should appear in picker list") option.tap() } private func createResidence( name: String, propertyType: String? = nil, street: String = "123 Test St", city: String = "TestCity", state: String = "TS", postal: String = "12345" ) { openResidenceForm() residenceForm.enterName(name) if let propertyType = propertyType { selectPropertyType(type: propertyType) } dismissKeyboard() fillAddressFields(street: street, city: city, state: state, postal: postal) residenceForm.save() createdResidenceNames.append(name) // Track for API cleanup if let items = TestAccountAPIClient.listResidences(token: session.token), let created = items.first(where: { $0.name.contains(name) }) { cleaner.trackResidence(created.id) } } private func findResidence(name: String) -> XCUIElement { return app.staticTexts.containing(NSPredicate(format: "label CONTAINS %@", name)).firstMatch } // MARK: - 1. Error/Validation Tests func test01_cannotCreateResidenceWithEmptyName() { openResidenceForm() // Leave name empty, fill only address fillAddressFields(street: "123 Test St", city: "TestCity", state: "TS", postal: "12345") // Scroll to save button if needed app.swipeUp() // Submit button should be disabled when name is empty (may be labeled "Add" or "Save") let saveButton = app.buttons[AccessibilityIdentifiers.Residence.saveButton].firstMatch _ = saveButton.waitForExistence(timeout: defaultTimeout) XCTAssertTrue(saveButton.exists, "Submit button should exist") XCTAssertFalse(saveButton.isEnabled, "Submit button should be disabled when name is empty") // Clean up: dismiss the form so next test starts on the list residenceForm.cancel() } func test02_cancelResidenceCreation() { openResidenceForm() // Fill some data let nameField = app.textFields[AccessibilityIdentifiers.Residence.nameField].firstMatch nameField.tap() // Wait for keyboard to appear before typing let keyboard = app.keyboards.firstMatch _ = keyboard.waitForExistence(timeout: 3) nameField.typeText("This will be canceled") // Tap cancel let cancelButton = app.buttons[AccessibilityIdentifiers.Residence.formCancelButton].firstMatch XCTAssertTrue(cancelButton.exists, "Cancel button should exist") cancelButton.tap() // Should be back on residences list let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch _ = residencesTab.waitForExistence(timeout: defaultTimeout) XCTAssertTrue(residencesTab.exists, "Should be back on residences list") // Residence should not exist let residence = findResidence(name: "This will be canceled") XCTAssertFalse(residence.exists, "Canceled residence should not exist") } // MARK: - 2. Creation Tests func test03_createResidenceWithMinimalData() { let timestamp = Int(Date().timeIntervalSince1970) let residenceName = "Minimal Home \(timestamp)" createResidence(name: residenceName) let residenceInList = findResidence(name: residenceName) XCTAssertTrue(residenceInList.waitForExistence(timeout: 10), "Residence should appear in list") } // test04_createResidenceWithAllPropertyTypes — removed: backend has no seeded residence types func test05_createMultipleResidencesInSequence() { let timestamp = Int(Date().timeIntervalSince1970) for i in 1...3 { let residenceName = "Sequential Home \(i) - \(timestamp)" createResidence(name: residenceName) navigateToResidences() } // Verify all residences exist for i in 1...3 { let residenceName = "Sequential Home \(i) - \(timestamp)" let residence = findResidence(name: residenceName) XCTAssertTrue(residence.exists, "Residence \(i) should exist in list") } } func test06_createResidenceWithVeryLongName() { let timestamp = Int(Date().timeIntervalSince1970) let longName = "This is an extremely long residence name that goes on and on and on to test how the system handles very long text input in the name field \(timestamp)" createResidence(name: longName) // Verify it appears (may be truncated in display) let residence = app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'extremely long residence'")).firstMatch XCTAssertTrue(residence.waitForExistence(timeout: 10), "Long name residence should exist") } func test07_createResidenceWithSpecialCharacters() { let timestamp = Int(Date().timeIntervalSince1970) let specialName = "Special !@#$%^&*() Home \(timestamp)" createResidence(name: specialName) let residence = findResidence(name: "Special") XCTAssertTrue(residence.waitForExistence(timeout: 10), "Residence with special chars should exist") } func test08_createResidenceWithEmojis() { let timestamp = Int(Date().timeIntervalSince1970) let emojiName = "Beach House \(timestamp)" createResidence(name: emojiName) let residence = findResidence(name: "Beach House") XCTAssertTrue(residence.waitForExistence(timeout: 10), "Residence with emojis should exist") } func test09_createResidenceWithInternationalCharacters() { let timestamp = Int(Date().timeIntervalSince1970) let internationalName = "Chateau Montreal \(timestamp)" createResidence(name: internationalName) let residence = findResidence(name: "Chateau") XCTAssertTrue(residence.waitForExistence(timeout: 10), "Residence with international chars should exist") } func test10_createResidenceWithVeryLongAddress() { let timestamp = Int(Date().timeIntervalSince1970) let residenceName = "Long Address Home \(timestamp)" createResidence( name: residenceName, street: "123456789 Very Long Street Name That Goes On And On Boulevard Apartment Complex Unit 42B", city: "VeryLongCityNameThatTestsTheLimit", state: "CA", postal: "12345-6789" ) let residence = findResidence(name: residenceName) XCTAssertTrue(residence.waitForExistence(timeout: 10), "Residence with long address should exist") } // MARK: - 3. Edit/Update Tests func test11_editResidenceName() { let timestamp = Int(Date().timeIntervalSince1970) let originalName = "Original Name \(timestamp)" let newName = "Edited Name \(timestamp)" // Create residence createResidence(name: originalName) navigateToResidences() // Find and tap residence let residence = findResidence(name: originalName) XCTAssertTrue(residence.waitForExistence(timeout: defaultTimeout), "Residence should exist") residence.tap() // Tap edit button let editButton = app.buttons[AccessibilityIdentifiers.Residence.editButton].firstMatch if editButton.waitForExistence(timeout: defaultTimeout) { editButton.tap() // Edit name let nameField = app.textFields[AccessibilityIdentifiers.Residence.nameField].firstMatch if nameField.waitForExistence(timeout: defaultTimeout) { nameField.clearAndEnterText(newName, app: app) // Save let saveButton = app.buttons[AccessibilityIdentifiers.Residence.saveButton].firstMatch if saveButton.exists { saveButton.tap() // Track new name createdResidenceNames.append(newName) // Verify new name appears navigateToResidences() let updatedResidence = findResidence(name: newName) XCTAssertTrue(updatedResidence.waitForExistence(timeout: defaultTimeout), "Residence should show updated name") } } } } func test12_updateAllResidenceFields() { let timestamp = Int(Date().timeIntervalSince1970) let originalName = "Update All Fields \(timestamp)" let newName = "All Fields Updated \(timestamp)" let newStreet = "999 Updated Avenue" let newCity = "NewCity" let newState = "NC" let newPostal = "99999" // Create residence with initial values createResidence(name: originalName, street: "123 Old St", city: "OldCity", state: "OC", postal: "11111") navigateToResidences() // Find and tap residence let residence = findResidence(name: originalName) XCTAssertTrue(residence.waitForExistence(timeout: defaultTimeout), "Residence should exist") residence.tap() // Tap edit button let editButton = app.buttons[AccessibilityIdentifiers.Residence.editButton].firstMatch XCTAssertTrue(editButton.waitForExistence(timeout: defaultTimeout), "Edit button should exist") editButton.tap() // Update name let nameField = app.textFields[AccessibilityIdentifiers.Residence.nameField].firstMatch XCTAssertTrue(nameField.waitForExistence(timeout: defaultTimeout), "Name field should exist") nameField.clearAndEnterText(newName, app: app) // Property type update skipped — backend has no seeded residence types // Dismiss keyboard from name edit, scroll to address fields dismissKeyboard() app.swipeUp() // Update address fields let streetField = app.textFields[AccessibilityIdentifiers.Residence.streetAddressField].firstMatch if streetField.waitForExistence(timeout: defaultTimeout) { streetField.clearAndEnterText(newStreet, app: app) dismissKeyboard() } let cityField = app.textFields[AccessibilityIdentifiers.Residence.cityField].firstMatch if cityField.waitForExistence(timeout: defaultTimeout) { cityField.clearAndEnterText(newCity, app: app) dismissKeyboard() } let stateField = app.textFields[AccessibilityIdentifiers.Residence.stateProvinceField].firstMatch if stateField.waitForExistence(timeout: defaultTimeout) { stateField.clearAndEnterText(newState, app: app) dismissKeyboard() } // Update postal code let postalField = app.textFields[AccessibilityIdentifiers.Residence.postalCodeField].firstMatch if postalField.exists { postalField.clearAndEnterText(newPostal, app: app) } // Scroll to save button app.swipeUp() // Save let saveButton = app.buttons[AccessibilityIdentifiers.Residence.saveButton].firstMatch _ = saveButton.waitForExistence(timeout: defaultTimeout) XCTAssertTrue(saveButton.exists, "Save button should exist") saveButton.tap() // Wait for form to dismiss after API call _ = saveButton.waitForNonExistence(timeout: defaultTimeout) // Track new name createdResidenceNames.append(newName) // Verify updated residence appears in list with new name navigateToResidences() let updatedResidence = findResidence(name: newName) XCTAssertTrue(updatedResidence.waitForExistence(timeout: defaultTimeout), "Residence should show updated name in list") // Name update verified in list — detail view doesn't display address fields } // MARK: - 4. View/Navigation Tests func test13_viewResidenceDetails() { let timestamp = Int(Date().timeIntervalSince1970) let residenceName = "Detail View Test \(timestamp)" // Create residence createResidence(name: residenceName) navigateToResidences() // Tap on residence let residence = findResidence(name: residenceName) XCTAssertTrue(residence.waitForExistence(timeout: defaultTimeout), "Residence should exist") residence.tap() // Verify detail view appears with edit button or tasks section let editButton = app.buttons[AccessibilityIdentifiers.Residence.editButton].firstMatch let tasksSection = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks' OR label CONTAINS[c] 'Maintenance'")).firstMatch _ = editButton.waitForExistence(timeout: defaultTimeout) XCTAssertTrue(editButton.exists || tasksSection.exists, "Detail view should show with edit button or tasks section") } func test14_navigateFromResidencesToOtherTabs() { // From Residences tab navigateToResidences() // Navigate to Tasks let tasksTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch XCTAssertTrue(tasksTab.exists, "Tasks tab should exist") tasksTab.tap() _ = tasksTab.waitForExistence(timeout: defaultTimeout) XCTAssertTrue(tasksTab.isSelected, "Should be on Tasks tab") // Navigate back to Residences let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch residencesTab.tap() _ = residencesTab.waitForExistence(timeout: defaultTimeout) XCTAssertTrue(residencesTab.isSelected, "Should be back on Residences tab") // Navigate to Contractors 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") // Back to Residences residencesTab.tap() _ = residencesTab.waitForExistence(timeout: defaultTimeout) XCTAssertTrue(residencesTab.isSelected, "Should be back on Residences tab again") } func test15_refreshResidencesList() { navigateToResidences() // Pull to refresh (if implemented) or use refresh button let refreshButton = app.navigationBars.buttons.containing(NSPredicate(format: "label CONTAINS 'arrow.clockwise' OR label CONTAINS 'refresh'")).firstMatch if refreshButton.waitForExistence(timeout: defaultTimeout) { refreshButton.tap() _ = app.activityIndicators.firstMatch.waitForNonExistence(timeout: defaultTimeout) } // Verify we're still on residences tab let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch XCTAssertTrue(residencesTab.isSelected, "Should still be on Residences tab after refresh") } // MARK: - 5. Persistence Tests func test16_residencePersistsAfterBackgroundingApp() { let timestamp = Int(Date().timeIntervalSince1970) let residenceName = "Persistence Test \(timestamp)" // Create residence createResidence(name: residenceName) navigateToResidences() // Verify residence exists var residence = findResidence(name: residenceName) XCTAssertTrue(residence.waitForExistence(timeout: defaultTimeout), "Residence should exist before backgrounding") // Background and reactivate app XCUIDevice.shared.press(.home) _ = app.wait(for: .runningForeground, timeout: 10) // Navigate back to residences navigateToResidences() // Verify residence still exists residence = findResidence(name: residenceName) XCTAssertTrue(residence.waitForExistence(timeout: defaultTimeout), "Residence should persist after backgrounding app") } }