import XCTest struct RebuildTestUser { let username: String let email: String let password: String } enum RebuildTestUserFactory { static func unique(prefix: String = "uit") -> RebuildTestUser { let stamp = Int(Date().timeIntervalSince1970) return RebuildTestUser( username: "\(prefix)_user_\(stamp)", email: "\(prefix)_\(stamp)@example.com", password: "Pass1234" ) } static var seeded: RebuildTestUser { RebuildTestUser(username: "testuser", email: "test@example.com", password: "TestPass123!") } } struct VerificationScreen { let app: XCUIApplication private var authCodeField: XCUIElement { app.textFields[AccessibilityIdentifiers.Authentication.verificationCodeField] } private var onboardingCodeField: XCUIElement { app.textFields[AccessibilityIdentifiers.Onboarding.verificationCodeField] } private var authVerifyButton: XCUIElement { app.buttons[AccessibilityIdentifiers.Authentication.verifyButton] } private var onboardingVerifyButton: XCUIElement { app.buttons[AccessibilityIdentifiers.Onboarding.verifyButton] } var codeField: XCUIElement { if authCodeField.exists { return authCodeField } return onboardingCodeField } var verifyButton: XCUIElement { if authVerifyButton.exists { return authVerifyButton } if onboardingVerifyButton.exists { return onboardingVerifyButton } return app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Verify'")).firstMatch } func waitForLoad(timeout: TimeInterval = 15, file: StaticString = #filePath, line: UInt = #line) { let loaded = authCodeField.waitForExistence(timeout: timeout) || onboardingCodeField.waitForExistence(timeout: timeout) || authVerifyButton.waitForExistence(timeout: timeout) || onboardingVerifyButton.waitForExistence(timeout: timeout) XCTAssertTrue(loaded, "Expected verification screen to load", file: file, line: line) } func enterCode(_ code: String) { codeField.waitForExistenceOrFail(timeout: 10) codeField.forceTap() codeField.typeText(code) } func submitCode() { verifyButton.waitForExistenceOrFail(timeout: 10) verifyButton.forceTap() } func tapLogoutIfAvailable() { let logout = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Logout' OR label CONTAINS[c] 'Log Out'")).firstMatch if logout.waitForExistence(timeout: 3) { logout.forceTap() } } } struct MainTabScreenObject { let app: XCUIApplication var tabBar: XCUIElement { app.tabBars.firstMatch } var mainRoot: XCUIElement { app.otherElements[UITestID.Root.mainTabs] } var residencesTab: XCUIElement { let byID = app.buttons[AccessibilityIdentifiers.Navigation.residencesTab] if byID.exists { return byID } return app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch } var profileTab: XCUIElement { let byID = app.buttons[AccessibilityIdentifiers.Navigation.profileTab] if byID.exists { return byID } return app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Profile'")).firstMatch } func waitForLoad(timeout: TimeInterval = 15) { let loaded = mainRoot.waitForExistence(timeout: timeout) || tabBar.waitForExistence(timeout: timeout) XCTAssertTrue(loaded, "Expected main app root to appear") } func goToResidences() { residencesTab.waitForExistenceOrFail(timeout: 10) residencesTab.forceTap() } func goToProfile() { profileTab.waitForExistenceOrFail(timeout: 10) profileTab.forceTap() } } struct ResidenceListScreen { let app: XCUIApplication var addButton: XCUIElement { let byID = app.buttons[AccessibilityIdentifiers.Residence.addButton] if byID.exists { return byID } return app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Add'")).firstMatch } var list: XCUIElement { app.otherElements[AccessibilityIdentifiers.Residence.residencesList] } var emptyState: XCUIElement { app.otherElements[AccessibilityIdentifiers.Residence.emptyStateView] } var residenceCard: XCUIElement { app.otherElements.matching(identifier: AccessibilityIdentifiers.Residence.residenceCard).firstMatch } func waitForLoad(timeout: TimeInterval = 15) { let deadline = Date().addingTimeInterval(timeout) var loaded = false repeat { loaded = list.exists || emptyState.exists || residenceCard.exists || addButton.exists || app.staticTexts["Residences"].exists if loaded { break } RunLoop.current.run(until: Date().addingTimeInterval(0.2)) } while Date() < deadline XCTAssertTrue(loaded, "Expected residences list screen to load") } func openCreateResidence() { addButton.waitForExistenceOrFail(timeout: 10) addButton.forceTap() } } struct ResidenceFormScreen { let app: XCUIApplication var nameField: XCUIElement { app.textFields[AccessibilityIdentifiers.Residence.nameField] } var saveButton: XCUIElement { app.buttons[AccessibilityIdentifiers.Residence.saveButton] } var cancelButton: XCUIElement { app.buttons[AccessibilityIdentifiers.Residence.formCancelButton] } func waitForLoad(timeout: TimeInterval = 15) { XCTAssertTrue(nameField.waitForExistence(timeout: timeout), "Expected residence form") } func enterName(_ value: String) { nameField.waitForExistenceOrFail(timeout: 10) nameField.forceTap() nameField.typeText(value) } func save() { saveButton.waitForExistenceOrFail(timeout: 10); saveButton.forceTap() } func cancel() { cancelButton.waitForExistenceOrFail(timeout: 10); cancelButton.forceTap() } } enum RebuildSessionAssertions { static func assertOnLogin(_ app: XCUIApplication, timeout: TimeInterval = 15, file: StaticString = #filePath, line: UInt = #line) { let login = LoginScreenObject(app: app) login.waitForLoad(timeout: timeout) XCTAssertTrue(app.textFields[UITestID.Auth.usernameField].exists, "Expected login state", file: file, line: line) } static func assertOnMainApp(_ app: XCUIApplication, timeout: TimeInterval = 15, file: StaticString = #filePath, line: UInt = #line) { let main = MainTabScreenObject(app: app) main.waitForLoad(timeout: timeout) XCTAssertTrue( app.otherElements[UITestID.Root.mainTabs].exists || main.tabBar.exists, "Expected main app state", file: file, line: line ) } static func assertOnVerification(_ app: XCUIApplication, timeout: TimeInterval = 15, file: StaticString = #filePath, line: UInt = #line) { let verify = VerificationScreen(app: app) verify.waitForLoad(timeout: timeout, file: file, line: line) } }