import XCTest /// Integration tests for residence CRUD against the real local backend. /// /// Uses a seeded admin account. Data is seeded via API and cleaned up in tearDown. final class ResidenceIntegrationTests: AuthenticatedUITestCase { override var needsAPISession: Bool { true } override var testCredentials: (username: String, password: String) { ("admin", "Test1234") } override var apiCredentials: (username: String, password: String) { ("admin", "Test1234") } // MARK: - Create Residence func testRES_CreateResidenceAppearsInList() { navigateToResidences() let residenceList = ResidenceListScreen(app: app) residenceList.waitForLoad(timeout: defaultTimeout) residenceList.openCreateResidence() let form = ResidenceFormScreen(app: app) form.waitForLoad(timeout: defaultTimeout) let uniqueName = "IntTest Residence \(Int(Date().timeIntervalSince1970))" form.enterName(uniqueName) form.save() let newResidence = app.staticTexts[uniqueName] XCTAssertTrue( newResidence.waitForExistence(timeout: loginTimeout), "Newly created residence should appear in the list" ) } // MARK: - Edit Residence func testRES_EditResidenceUpdatesInList() { // Seed a residence via API so we have a known target to edit let seeded = cleaner.seedResidence(name: "Edit Target \(Int(Date().timeIntervalSince1970))") navigateToResidences() pullToRefresh() let residenceList = ResidenceListScreen(app: app) residenceList.waitForLoad(timeout: defaultTimeout) // Find and tap the seeded residence let card = app.staticTexts[seeded.name] pullToRefreshUntilVisible(card, maxRetries: 3) card.waitForExistenceOrFail(timeout: loginTimeout) card.forceTap() // Tap edit button on detail view let editButton = app.buttons[AccessibilityIdentifiers.Residence.editButton] editButton.waitForExistenceOrFail(timeout: defaultTimeout) editButton.forceTap() let form = ResidenceFormScreen(app: app) form.waitForLoad(timeout: defaultTimeout) // Clear and re-enter name let nameField = form.nameField nameField.waitUntilHittable(timeout: 10).tap() nameField.press(forDuration: 1.0) let selectAll = app.menuItems["Select All"] if selectAll.waitForExistence(timeout: 2) { selectAll.tap() } let updatedName = "Updated Res \(Int(Date().timeIntervalSince1970))" nameField.typeText(updatedName) form.save() let updatedText = app.staticTexts[updatedName] XCTAssertTrue( updatedText.waitForExistence(timeout: loginTimeout), "Updated residence name should appear after edit" ) } // MARK: - RES-007: Primary Residence func test18_setPrimaryResidence() { // Seed two residences via API; the second one will be promoted to primary let firstResidence = cleaner.seedResidence(name: "Primary Test A \(Int(Date().timeIntervalSince1970))") let secondResidence = cleaner.seedResidence(name: "Primary Test B \(Int(Date().timeIntervalSince1970))") navigateToResidences() pullToRefresh() let residenceList = ResidenceListScreen(app: app) residenceList.waitForLoad(timeout: defaultTimeout) // Open the second residence's detail let secondCard = app.staticTexts[secondResidence.name] pullToRefreshUntilVisible(secondCard, maxRetries: 3) secondCard.waitForExistenceOrFail(timeout: loginTimeout) secondCard.forceTap() // Tap edit let editButton = app.buttons[AccessibilityIdentifiers.Residence.editButton] editButton.waitForExistenceOrFail(timeout: defaultTimeout) editButton.forceTap() let form = ResidenceFormScreen(app: app) form.waitForLoad(timeout: defaultTimeout) // Find and toggle the "is primary" toggle let isPrimaryToggle = app.switches[AccessibilityIdentifiers.Residence.isPrimaryToggle] isPrimaryToggle.scrollIntoView(in: app.scrollViews.firstMatch) isPrimaryToggle.waitForExistenceOrFail(timeout: defaultTimeout) // Toggle it on (value "0" means off, "1" means on) if (isPrimaryToggle.value as? String) == "0" { isPrimaryToggle.forceTap() } form.save() // After saving, a primary indicator should be visible — either a label, // badge, or the toggle being on in the refreshed detail view. let primaryIndicator = app.staticTexts.containing( NSPredicate(format: "label CONTAINS[c] 'Primary'") ).firstMatch let primaryBadge = app.images.containing( NSPredicate(format: "label CONTAINS[c] 'Primary'") ).firstMatch let indicatorVisible = primaryIndicator.waitForExistence(timeout: loginTimeout) || primaryBadge.waitForExistence(timeout: 3) XCTAssertTrue( indicatorVisible, "A primary residence indicator should appear after setting '\(secondResidence.name)' as primary" ) // Clean up: remove unused firstResidence id from tracking (already tracked via cleaner) _ = firstResidence } // MARK: - OFF-004: Double Submit Protection func test19_doubleSubmitProtection() { navigateToResidences() let residenceList = ResidenceListScreen(app: app) residenceList.waitForLoad(timeout: defaultTimeout) residenceList.openCreateResidence() let form = ResidenceFormScreen(app: app) form.waitForLoad(timeout: defaultTimeout) let uniqueName = "DoubleSubmit \(Int(Date().timeIntervalSince1970))" form.enterName(uniqueName) // Rapidly tap save twice to test double-submit protection let saveButton = app.buttons[AccessibilityIdentifiers.Residence.saveButton] saveButton.scrollIntoView(in: app.scrollViews.firstMatch) saveButton.forceTap() // Second tap immediately after — if the button is already disabled this will be a no-op if saveButton.isHittable { saveButton.forceTap() } // Wait for the form to dismiss (sheet closes, we return to the list) let formDismissed = saveButton.waitForNonExistence(timeout: loginTimeout) XCTAssertTrue(formDismissed, "Form should dismiss after save") // Back on the residences list — count how many cells with the unique name exist let matchingTexts = app.staticTexts.matching( NSPredicate(format: "label == %@", uniqueName) ) // Allow time for the list to fully load _ = app.staticTexts[uniqueName].waitForExistence(timeout: defaultTimeout) XCTAssertEqual( matchingTexts.count, 1, "Only one residence named '\(uniqueName)' should exist — double-submit protection should prevent duplicates" ) // Track the created residence for cleanup if let residences = TestAccountAPIClient.listResidences(token: session.token) { if let created = residences.first(where: { $0.name == uniqueName }) { cleaner.trackResidence(created.id) } } } // MARK: - Delete Residence func testRES_DeleteResidenceRemovesFromList() { // Seed a residence via API — don't track it since we'll delete through the UI let deleteName = "Delete Me \(Int(Date().timeIntervalSince1970))" TestDataSeeder.createResidence(token: session.token, name: deleteName) navigateToResidences() pullToRefresh() let residenceList = ResidenceListScreen(app: app) residenceList.waitForLoad(timeout: defaultTimeout) // Find and tap the seeded residence let target = app.staticTexts[deleteName] pullToRefreshUntilVisible(target, maxRetries: 3) target.waitForExistenceOrFail(timeout: loginTimeout) target.forceTap() // Tap delete button let deleteButton = app.buttons[AccessibilityIdentifiers.Residence.deleteButton] deleteButton.waitForExistenceOrFail(timeout: defaultTimeout) deleteButton.forceTap() // Confirm deletion in alert let confirmButton = app.buttons[AccessibilityIdentifiers.Alert.confirmButton] let alertDelete = app.alerts.buttons.containing( NSPredicate(format: "label CONTAINS[c] 'Delete' OR label CONTAINS[c] 'Confirm'") ).firstMatch if confirmButton.waitForExistence(timeout: defaultTimeout) { confirmButton.tap() } else if alertDelete.waitForExistence(timeout: defaultTimeout) { alertDelete.tap() } let deletedResidence = app.staticTexts[deleteName] XCTAssertTrue( deletedResidence.waitForNonExistence(timeout: loginTimeout), "Deleted residence should no longer appear in the list" ) } }