UI test infrastructure overhaul — 58% to 96% pass rate (231/241)

Major infrastructure changes:
- BaseUITestCase: per-suite app termination via class setUp() prevents
  stale state when parallel clones share simulators
- relaunchBetweenTests override for suites that modify login/onboarding state
- focusAndType: dedicated SecureTextField path handles iOS strong password
  autofill suggestions (Choose My Own Password / Not Now dialogs)
- LoginScreenObject: tapSignUp/tapForgotPassword use scrollIntoView for
  offscreen buttons instead of simple swipeUp
- Removed all coordinate taps from ForgotPasswordScreen, VerifyResetCodeScreen,
  ResetPasswordScreen (Rule 3 compliance)
- Removed all usleep calls from screen objects (Rule 14 compliance)

App fixes exposed by tests:
- ContractorsListView: added onDismiss to sheet for list refresh after save
- AllTasksView: added Task.RefreshButton accessibility identifier
- AccessibilityIdentifiers: added Task.refreshButton
- DocumentsWarrantiesView: onDismiss handler for document list refresh
- Various form views: textContentType, submitLabel, onSubmit for keyboard flow

Test fixes:
- PasswordResetTests: handle auto-login after reset (app skips success screen)
- AuthenticatedUITestCase: refreshTasks() helper for kanban toolbar button
- All pre-login suites use relaunchBetweenTests for test independence
- Deleted dead code: AuthenticatedTestCase, SeededTestData, SeedTests,
  CleanupTests, old Suite0/2/3, Suite1_RegistrationRebuildTests

10 remaining failures: 5 iOS strong password autofill (simulator env),
3 pull-to-refresh gesture on empty lists, 2 feature coverage edge cases.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-03-23 15:05:37 -05:00
parent 0ca4a44bac
commit 4df8707b92
67 changed files with 3085 additions and 4853 deletions

View File

@@ -3,9 +3,10 @@ import XCTest
/// Integration tests for residence CRUD against the real local backend.
///
/// Uses a seeded admin account. Data is seeded via API and cleaned up in tearDown.
final class ResidenceIntegrationTests: AuthenticatedTestCase {
override var useSeededAccount: Bool { true }
final class ResidenceIntegrationTests: AuthenticatedUITestCase {
override var needsAPISession: Bool { true }
override var testCredentials: (username: String, password: String) { ("admin", "test1234") }
override var apiCredentials: (username: String, password: String) { ("admin", "test1234") }
// MARK: - Create Residence
@@ -26,7 +27,7 @@ final class ResidenceIntegrationTests: AuthenticatedTestCase {
let newResidence = app.staticTexts[uniqueName]
XCTAssertTrue(
newResidence.waitForExistence(timeout: longTimeout),
newResidence.waitForExistence(timeout: loginTimeout),
"Newly created residence should appear in the list"
)
}
@@ -38,13 +39,15 @@ final class ResidenceIntegrationTests: AuthenticatedTestCase {
let seeded = cleaner.seedResidence(name: "Edit Target \(Int(Date().timeIntervalSince1970))")
navigateToResidences()
pullToRefresh()
let residenceList = ResidenceListScreen(app: app)
residenceList.waitForLoad(timeout: defaultTimeout)
// Find and tap the seeded residence
let card = app.staticTexts[seeded.name]
card.waitForExistenceOrFail(timeout: longTimeout)
pullToRefreshUntilVisible(card, maxRetries: 3)
card.waitForExistenceOrFail(timeout: loginTimeout)
card.forceTap()
// Tap edit button on detail view
@@ -70,7 +73,7 @@ final class ResidenceIntegrationTests: AuthenticatedTestCase {
let updatedText = app.staticTexts[updatedName]
XCTAssertTrue(
updatedText.waitForExistence(timeout: longTimeout),
updatedText.waitForExistence(timeout: loginTimeout),
"Updated residence name should appear after edit"
)
}
@@ -83,13 +86,15 @@ final class ResidenceIntegrationTests: AuthenticatedTestCase {
let secondResidence = cleaner.seedResidence(name: "Primary Test B \(Int(Date().timeIntervalSince1970))")
navigateToResidences()
pullToRefresh()
let residenceList = ResidenceListScreen(app: app)
residenceList.waitForLoad(timeout: defaultTimeout)
// Open the second residence's detail
let secondCard = app.staticTexts[secondResidence.name]
secondCard.waitForExistenceOrFail(timeout: longTimeout)
pullToRefreshUntilVisible(secondCard, maxRetries: 3)
secondCard.waitForExistenceOrFail(timeout: loginTimeout)
secondCard.forceTap()
// Tap edit
@@ -122,7 +127,7 @@ final class ResidenceIntegrationTests: AuthenticatedTestCase {
NSPredicate(format: "label CONTAINS[c] 'Primary'")
).firstMatch
let indicatorVisible = primaryIndicator.waitForExistence(timeout: longTimeout)
let indicatorVisible = primaryIndicator.waitForExistence(timeout: loginTimeout)
|| primaryBadge.waitForExistence(timeout: 3)
XCTAssertTrue(
@@ -160,7 +165,7 @@ final class ResidenceIntegrationTests: AuthenticatedTestCase {
}
// Wait for the form to dismiss (sheet closes, we return to the list)
let formDismissed = saveButton.waitForNonExistence(timeout: longTimeout)
let formDismissed = saveButton.waitForNonExistence(timeout: loginTimeout)
XCTAssertTrue(formDismissed, "Form should dismiss after save")
// Back on the residences list count how many cells with the unique name exist
@@ -192,13 +197,15 @@ final class ResidenceIntegrationTests: AuthenticatedTestCase {
TestDataSeeder.createResidence(token: session.token, name: deleteName)
navigateToResidences()
pullToRefresh()
let residenceList = ResidenceListScreen(app: app)
residenceList.waitForLoad(timeout: defaultTimeout)
// Find and tap the seeded residence
let target = app.staticTexts[deleteName]
target.waitForExistenceOrFail(timeout: longTimeout)
pullToRefreshUntilVisible(target, maxRetries: 3)
target.waitForExistenceOrFail(timeout: loginTimeout)
target.forceTap()
// Tap delete button
@@ -212,15 +219,15 @@ final class ResidenceIntegrationTests: AuthenticatedTestCase {
NSPredicate(format: "label CONTAINS[c] 'Delete' OR label CONTAINS[c] 'Confirm'")
).firstMatch
if confirmButton.waitForExistence(timeout: shortTimeout) {
if confirmButton.waitForExistence(timeout: defaultTimeout) {
confirmButton.tap()
} else if alertDelete.waitForExistence(timeout: shortTimeout) {
} else if alertDelete.waitForExistence(timeout: defaultTimeout) {
alertDelete.tap()
}
let deletedResidence = app.staticTexts[deleteName]
XCTAssertTrue(
deletedResidence.waitForNonExistence(timeout: longTimeout),
deletedResidence.waitForNonExistence(timeout: loginTimeout),
"Deleted residence should no longer appear in the list"
)
}