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.
This commit is contained in:
@@ -75,29 +75,87 @@ final class Suite8_DocumentWarrantyTests: AuthenticatedUITestCase {
|
||||
|
||||
/// Select a property from the residence picker. Fails the test if picker is missing or empty.
|
||||
private func selectProperty(file: StaticString = #filePath, line: UInt = #line) {
|
||||
// Look up the seeded residence name so we can match it by text in
|
||||
// whichever picker variant iOS renders (menu, list, or wheel).
|
||||
let residences = TestAccountAPIClient.listResidences(token: session.token) ?? []
|
||||
let residenceName = residences.first?.name
|
||||
|
||||
let pickerButton = app.buttons[AccessibilityIdentifiers.Document.residencePicker].firstMatch
|
||||
pickerButton.waitForExistenceOrFail(timeout: navigationTimeout, message: "Property picker should exist", file: file, line: line)
|
||||
|
||||
pickerButton.tap()
|
||||
|
||||
// SwiftUI Picker in Form pushes a selection list — find any row to select
|
||||
// Try menu items first (menu style), then static texts (list style)
|
||||
// Fast path: the residence option is often rendered as a plain Button
|
||||
// or StaticText whose label is the residence name itself. Finding it
|
||||
// by text works across menu, list, and wheel picker variants.
|
||||
if let name = residenceName {
|
||||
let byButton = app.buttons[name].firstMatch
|
||||
if byButton.waitForExistence(timeout: 3) && byButton.isHittable {
|
||||
byButton.tap()
|
||||
_ = docForm.titleField.waitForExistence(timeout: navigationTimeout)
|
||||
return
|
||||
}
|
||||
let byText = app.staticTexts[name].firstMatch
|
||||
if byText.exists && byText.isHittable {
|
||||
byText.tap()
|
||||
_ = docForm.titleField.waitForExistence(timeout: navigationTimeout)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// SwiftUI Picker in Form renders either a menu (iOS 18+ default) or a
|
||||
// pushed selection list. Detecting the menu requires a slightly longer
|
||||
// wait because the dropdown animates in after the tap. Also: the form
|
||||
// rows themselves are `cells`, so we can't use `cells.firstMatch` to
|
||||
// detect list mode — we must wait longer for a real menu before
|
||||
// falling back.
|
||||
let menuItem = app.menuItems.firstMatch
|
||||
if menuItem.waitForExistence(timeout: navigationTimeout) {
|
||||
// Give the menu a bit longer to animate; 5s covers the usual case.
|
||||
if menuItem.waitForExistence(timeout: 5) {
|
||||
// Tap the last menu item (the residence option; the placeholder is
|
||||
// index 0 and carries the "Select a Property" label).
|
||||
let allItems = app.menuItems.allElementsBoundByIndex
|
||||
allItems[max(allItems.count - 1, 0)].tap()
|
||||
let target = allItems.last ?? menuItem
|
||||
if target.isHittable {
|
||||
target.tap()
|
||||
} else {
|
||||
target.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||
}
|
||||
// Ensure the menu actually dismissed; a lingering overlay blocks
|
||||
// hit-testing on the form below.
|
||||
_ = app.menuItems.firstMatch.waitForNonExistence(timeout: 2)
|
||||
return
|
||||
} else {
|
||||
// List-style picker — find a cell/row with a residence name
|
||||
// List-style picker — find a cell/row with a residence name.
|
||||
// Cells can take a moment to become hittable during the push
|
||||
// animation; retry the tap until the picker dismisses (titleField
|
||||
// reappears on the form) or the attempt budget runs out.
|
||||
let cells = app.cells
|
||||
guard cells.firstMatch.waitForExistence(timeout: navigationTimeout) else {
|
||||
XCTFail("No residence options appeared in picker", file: file, line: line)
|
||||
return
|
||||
}
|
||||
// Tap the first non-placeholder cell
|
||||
if cells.count > 1 {
|
||||
cells.element(boundBy: 1).tap()
|
||||
} else {
|
||||
cells.element(boundBy: 0).tap()
|
||||
|
||||
let hittable = NSPredicate(format: "isHittable == true")
|
||||
for attempt in 0..<5 {
|
||||
let targetCell = cells.count > 1 ? cells.element(boundBy: 1) : cells.element(boundBy: 0)
|
||||
guard targetCell.exists else {
|
||||
RunLoop.current.run(until: Date().addingTimeInterval(0.3))
|
||||
continue
|
||||
}
|
||||
_ = XCTWaiter().wait(
|
||||
for: [XCTNSPredicateExpectation(predicate: hittable, object: targetCell)],
|
||||
timeout: 2.0 + Double(attempt)
|
||||
)
|
||||
if targetCell.isHittable {
|
||||
targetCell.tap()
|
||||
if docForm.titleField.waitForExistence(timeout: 2) { break }
|
||||
}
|
||||
// Reopen picker if it dismissed without selection.
|
||||
if docForm.titleField.exists, attempt < 4, pickerButton.exists, pickerButton.isHittable {
|
||||
pickerButton.tap()
|
||||
_ = cells.firstMatch.waitForExistence(timeout: 3)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user