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.
222 lines
8.2 KiB
Swift
222 lines
8.2 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.focusAndType(code, app: app)
|
|
}
|
|
|
|
func submitCode() {
|
|
verifyButton.waitForExistenceOrFail(timeout: 10)
|
|
verifyButton.forceTap()
|
|
}
|
|
|
|
func tapLogoutIfAvailable() {
|
|
let logout = app.buttons[AccessibilityIdentifiers.Profile.logoutButton].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 tasksTab: XCUIElement {
|
|
let byID = app.buttons[AccessibilityIdentifiers.Navigation.tasksTab]
|
|
if byID.exists { return byID }
|
|
return app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch
|
|
}
|
|
|
|
var contractorsTab: XCUIElement {
|
|
let byID = app.buttons[AccessibilityIdentifiers.Navigation.contractorsTab]
|
|
if byID.exists { return byID }
|
|
return app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Contractors'")).firstMatch
|
|
}
|
|
|
|
var documentsTab: XCUIElement {
|
|
let byID = app.buttons[AccessibilityIdentifiers.Navigation.documentsTab]
|
|
if byID.exists { return byID }
|
|
return app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Doc'")).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 goToTasks() {
|
|
tasksTab.waitForExistenceOrFail(timeout: 10)
|
|
tasksTab.forceTap()
|
|
}
|
|
|
|
func goToContractors() {
|
|
contractorsTab.waitForExistenceOrFail(timeout: 10)
|
|
contractorsTab.forceTap()
|
|
}
|
|
|
|
func goToDocuments() {
|
|
documentsTab.waitForExistenceOrFail(timeout: 10)
|
|
documentsTab.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.focusAndType(value, app: app)
|
|
}
|
|
|
|
func save() {
|
|
KeyboardDismisser.dismiss(app: app)
|
|
if !saveButton.exists || !saveButton.isHittable { app.swipeUp() }
|
|
saveButton.waitForExistenceOrFail(timeout: 10)
|
|
saveButton.forceTap()
|
|
_ = saveButton.waitForNonExistence(timeout: 15)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|