Files
honeyDueKMP/iosApp/HoneyDueUITests/Framework/RebuildSupport.swift
treyt 5c360a2796 Rearchitect UI test suite for complete, non-flaky coverage against live API
- Migrate Suite4-10, SmokeTests, NavigationCriticalPathTests to AuthenticatedTestCase
  with seeded admin account and real backend login
- Add 34 accessibility identifiers across 11 app views (task completion, profile,
  notifications, theme, join residence, manage users, forms)
- Create FeatureCoverageTests (14 tests) covering previously untested features:
  profile edit, theme selection, notification prefs, task completion, manage users,
  join residence, task templates
- Create MultiUserSharingTests (18 API tests) and MultiUserSharingUITests (8 XCUI
  tests) for full cross-user residence sharing lifecycle
- Add cleanup infrastructure: SuiteZZ_CleanupTests auto-wipes test data after runs,
  cleanup_test_data.sh script for manual reset via admin API
- Add share code API methods to TestAccountAPIClient (generateShareCode, joinWithCode,
  getShareCode, listResidenceUsers, removeUser)
- Fix app bugs found by tests:
  - ResidencesListView join callback now uses forceRefresh:true
  - APILayer invalidates task cache when residence count changes
  - AllTasksView auto-reloads tasks when residence list changes
- Fix test quality: keyboard focus waits, Save/Add button label matching,
  Documents tab label (Docs), remove API verification from UI tests
- DataLayerTests and PasswordResetTests now verify through UI, not API calls

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 17:32:13 -05:00

184 lines
7.0 KiB
Swift

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].firstMatch
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)
}
}