Files
honeyDueKMP/iosApp/HoneyDueUITests/Framework/TestAccountManager.swift
Trey T a4d66c6ed1 Stabilize UI test suite — 39% → 98%+ pass rate
Fix root causes uncovered across repeated parallel runs:

- Admin seed password "test1234" failed backend complexity (needs
  uppercase). Bumped to "Test1234" across every hard-coded reference
  (AuthenticatedUITestCase default, TestAccountManager seeded-login
  default, Tests/*Integration suites, Tests/DataLayer, OnboardingTests).

- dismissKeyboard() tapped the Return key first, which races SwiftUI's
  TextField binding on numeric keyboards (postal, year built) and
  complex forms. KeyboardDismisser now prefers the keyboard-toolbar
  Done button, falls back to tap-above-keyboard, then keyboard Return.
  BaseUITestCase.clearAndEnterText uses the same helper.

- Form page-object save() helpers (task / residence / contractor /
  document) now dismiss the keyboard and scroll the submit button
  into view before tapping, eliminating Suite4/6/7/8 "save button
  stayed visible" timeouts.

- Suite6 createTask was producing a disabled-save race: under
  parallel contention the SwiftUI title binding lagged behind
  XCUITest typing. Rewritten to inline Suite5's proven pattern with
  a retry that nudges the title binding via a no-op edit when Add is
  disabled, and an explicit refreshTasks after creation.

- Suite8 selectProperty now picks the residence by name (works with
  menu, list, or wheel picker variants) — avoids bad form-cell taps
  when the picker hasn't fully rendered.

- run_ui_tests.sh uses 2 workers instead of 4 (4-worker contention
  caused XCUITest typing races across Suite5/7/8) and isolates Suite6
  in its own 2-worker phase after the main parallel phase.

- Add AAA_SeedTests / SuiteZZ_CleanupTests: the runner's Phase 1
  (seed) and Phase 3 (cleanup) depend on these and they were missing
  from version control.
2026-04-15 08:38:31 -05:00

128 lines
4.2 KiB
Swift

import Foundation
import XCTest
/// High-level account lifecycle management for UI tests.
enum TestAccountManager {
// MARK: - Credential Generation
/// Generate unique credentials with a timestamp + random suffix to avoid collisions.
static func uniqueCredentials(prefix: String = "uit") -> (username: String, email: String, password: String) {
let stamp = Int(Date().timeIntervalSince1970)
let random = Int.random(in: 1000...9999)
let username = "\(prefix)_\(stamp)_\(random)"
let email = "\(username)@test.example.com"
let password = "Pass\(stamp)!"
return (username, email, password)
}
// MARK: - Account Creation
/// Create a verified account via the backend API. Returns a ready-to-use session.
/// Calls `XCTFail` and returns nil if any step fails.
static func createVerifiedAccount(
file: StaticString = #filePath,
line: UInt = #line
) -> TestSession? {
let creds = uniqueCredentials()
guard let session = TestAccountAPIClient.createVerifiedAccount(
username: creds.username,
email: creds.email,
password: creds.password
) else {
XCTFail("Failed to create verified account for \(creds.username)", file: file, line: line)
return nil
}
return session
}
/// Create an unverified account (register only, no email verification).
/// Useful for testing the verification gate.
static func createUnverifiedAccount(
file: StaticString = #filePath,
line: UInt = #line
) -> TestSession? {
let creds = uniqueCredentials()
guard let response = TestAccountAPIClient.register(
username: creds.username,
email: creds.email,
password: creds.password
) else {
XCTFail("Failed to register unverified account for \(creds.username)", file: file, line: line)
return nil
}
return TestSession(
token: response.token,
user: response.user,
username: creds.username,
password: creds.password
)
}
// MARK: - Seeded Accounts
/// Login with a pre-seeded account that already exists in the database.
static func loginSeededAccount(
username: String = "admin",
password: String = "Test1234",
file: StaticString = #filePath,
line: UInt = #line
) -> TestSession? {
guard let response = TestAccountAPIClient.login(username: username, password: password) else {
XCTFail("Failed to login seeded account '\(username)'", file: file, line: line)
return nil
}
return TestSession(
token: response.token,
user: response.user,
username: username,
password: password
)
}
// MARK: - Password Reset
/// Execute the full forgotverifyreset cycle via the backend API.
static func resetPassword(
email: String,
newPassword: String,
file: StaticString = #filePath,
line: UInt = #line
) -> Bool {
guard TestAccountAPIClient.forgotPassword(email: email) != nil else {
XCTFail("Forgot password request failed for \(email)", file: file, line: line)
return false
}
guard let verifyResponse = TestAccountAPIClient.verifyResetCode(email: email) else {
XCTFail("Verify reset code failed for \(email)", file: file, line: line)
return false
}
guard TestAccountAPIClient.resetPassword(resetToken: verifyResponse.resetToken, newPassword: newPassword) != nil else {
XCTFail("Reset password failed for \(email)", file: file, line: line)
return false
}
return true
}
// MARK: - Token Management
/// Invalidate a session token via the logout API.
static func invalidateToken(
_ session: TestSession,
file: StaticString = #filePath,
line: UInt = #line
) {
if TestAccountAPIClient.logout(token: session.token) == nil {
XCTFail("Failed to invalidate token for \(session.username)", file: file, line: line)
}
}
}