import XCTest /// Residence MUTATION coverage: validation, creation (incl. edge-case names and /// addresses), and editing. /// /// Migrated from the mutation half of Suite4_ComprehensiveResidenceTests. The /// view/navigation/refresh/persistence tests from that suite live in /// `ResidenceUITests`. /// /// Per-test isolation comes from `AuthenticatedUITestCase` (fresh account per /// test, deleted in teardown). These tests CREATE residences through the UI, so /// they need no seeded precondition — creation doesn't require existing data. final class ResidenceManagementUITests: AuthenticatedUITestCase { // Test data tracking — names created through the UI, reconciled to IDs for // API cleanup in tearDown. 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 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 in source: 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 } }