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:
Trey T
2026-04-15 08:38:31 -05:00
parent 9ececfa48a
commit a4d66c6ed1
17 changed files with 388 additions and 59 deletions

View File

@@ -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)
}
}
}