import XCTest /// Residence READ / navigation / list / detail behaviour. /// /// Merged from three legacy suites: /// - ResidenceIntegrationTests (CRUD round-trips against the real backend) /// - Suite3_ResidenceRebuildTests (rebuilt navigation/list/detail coverage — /// manual login scaffolding removed; the base now provides a logged-in session) /// - Suite4_ComprehensiveResidenceTests (the view/navigation/refresh/persistence tests) /// /// Per-test isolation: `AuthenticatedUITestCase` mints a fresh, pre-verified /// account, logs in, and deletes it in teardown. A fresh account starts EMPTY, /// so tests that need to SEE a pre-existing residence seed it in /// `seedAccountPreconditions` (before login) and reference `seededResidence`. final class ResidenceUITests: AuthenticatedUITestCase { // MARK: - Page Objects private var residenceList: ResidenceListScreen { ResidenceListScreen(app: app) } private var residenceForm: ResidenceFormScreen { ResidenceFormScreen(app: app) } // MARK: - Helpers private func findResidence(name: String) -> XCUIElement { app.staticTexts.containing(NSPredicate(format: "label CONTAINS %@", name)).firstMatch } // Suite3's createResidence helper, stripped of the manual login (the base // now lands us on the main app already authenticated). @discardableResult private func createResidenceViaUI(name: String) -> String { navigateToResidences() let list = ResidenceListScreen(app: app) list.waitForLoad(timeout: defaultTimeout) list.openCreateResidence() let form = ResidenceFormScreen(app: app) form.waitForLoad(timeout: defaultTimeout) form.enterName(name) form.save() return name } // MARK: - Create (round-trip) — from ResidenceIntegrationTests func testRES_CreateResidenceAppearsInList() { navigateToResidences() let list = ResidenceListScreen(app: app) list.waitForLoad(timeout: defaultTimeout) list.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 (round-trip) — from ResidenceIntegrationTests func testRES_EditResidenceUpdatesInList() { // Seed a residence via API so we have a known target to edit, then // pull-to-refresh so the fresh account's empty list picks it up. let seeded = cleaner.seedResidence(name: "Edit Target \(Int(Date().timeIntervalSince1970))") navigateToResidences() pullToRefresh() let list = ResidenceListScreen(app: app) list.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: - Set Primary (RES-007) — from ResidenceIntegrationTests 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 list = ResidenceListScreen(app: app) list.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: - Double Submit Protection (OFF-004) — from ResidenceIntegrationTests func test19_doubleSubmitProtection() { navigateToResidences() let list = ResidenceListScreen(app: app) list.waitForLoad(timeout: defaultTimeout) list.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 (round-trip) — from ResidenceIntegrationTests 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 list = ResidenceListScreen(app: app) list.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" ) } // MARK: - Rebuilt navigation / list / detail — from Suite3 // // The original Suite3 ran on BaseUITestCase and logged in manually inside // each test (a `loginAndOpenResidences` helper plus a verification-gate // loop). The base class now provides a logged-in session, so that // scaffolding is removed and only the residence assertions remain. func testR301_authenticatedPreconditionCanReachMainApp() throws { navigateToResidences() RebuildSessionAssertions.assertOnMainApp(app, timeout: defaultTimeout) } func testR302_residencesTabIsPresentAndNavigable() throws { navigateToResidences() let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch XCTAssertTrue(residencesTab.exists, "Residences tab should exist") } func testR303_residencesListLoadsAfterTabSelection() throws { navigateToResidences() let list = ResidenceListScreen(app: app) list.waitForLoad(timeout: defaultTimeout) XCTAssertTrue(list.addButton.exists, "Add residence button should be visible") } func testR304_openAddResidenceFormFromResidencesList() throws { navigateToResidences() let list = ResidenceListScreen(app: app) list.waitForLoad(timeout: defaultTimeout) list.openCreateResidence() let form = ResidenceFormScreen(app: app) form.waitForLoad(timeout: defaultTimeout) XCTAssertTrue(form.saveButton.exists, "Residence save button should exist") } func testR305_cancelAddResidenceReturnsToResidenceList() throws { navigateToResidences() let list = ResidenceListScreen(app: app) list.waitForLoad(timeout: defaultTimeout) list.openCreateResidence() let form = ResidenceFormScreen(app: app) form.waitForLoad(timeout: defaultTimeout) form.cancel() list.waitForLoad(timeout: defaultTimeout) } func testR306_createResidenceMinimalDataSubmitsSuccessfully() throws { let name = "UITest Home \(Int(Date().timeIntervalSince1970))" _ = createResidenceViaUI(name: name) let created = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] %@", name)).firstMatch XCTAssertTrue(created.waitForExistence(timeout: loginTimeout), "Created residence should appear in list") } func testR307_newResidenceAppearsInResidenceList() throws { let name = "UITest Verify \(Int(Date().timeIntervalSince1970))" _ = createResidenceViaUI(name: name) let created = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] %@", name)).firstMatch XCTAssertTrue(created.waitForExistence(timeout: loginTimeout), "New residence should be visible in residences list") } func testR308_openResidenceDetailsFromResidenceList() throws { let name = "UITest Detail \(Int(Date().timeIntervalSince1970))" _ = createResidenceViaUI(name: name) let row = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] %@", name)).firstMatch row.waitForExistenceOrFail(timeout: loginTimeout).forceTap() let edit = app.buttons[AccessibilityIdentifiers.Residence.editButton] let delete = app.buttons[AccessibilityIdentifiers.Residence.deleteButton] let loaded = edit.waitForExistence(timeout: defaultTimeout) || delete.waitForExistence(timeout: defaultTimeout) XCTAssertTrue(loaded, "Residence details should expose edit or delete actions") } func testR309_navigationAcrossPrimaryTabsAndBackToResidences() throws { navigateToResidences() let tabBar = app.tabBars.firstMatch tabBar.waitForExistenceOrFail(timeout: defaultTimeout) let tasksTab = tabBar.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch XCTAssertTrue(tasksTab.exists, "Tasks tab should exist") tasksTab.forceTap() let contractorsTab = tabBar.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Contractors'")).firstMatch XCTAssertTrue(contractorsTab.exists, "Contractors tab should exist") contractorsTab.forceTap() let residencesTab = tabBar.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch residencesTab.forceTap() let list = ResidenceListScreen(app: app) list.waitForLoad(timeout: defaultTimeout) } // MARK: - View / navigation / refresh / persistence — from Suite4 func test13_viewResidenceDetails() { let timestamp = Int(Date().timeIntervalSince1970) let residenceName = "Detail View Test \(timestamp)" // Create residence through the UI, then open its detail _ = createResidenceViaUI(name: residenceName) navigateToResidences() 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") } func test16_residencePersistsAfterBackgroundingApp() { let timestamp = Int(Date().timeIntervalSince1970) let residenceName = "Persistence Test \(timestamp)" // Create residence through the UI _ = createResidenceViaUI(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") } }