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

@@ -12,47 +12,40 @@ import XCTest
///
/// IMPORTANT: These tests create real data and require network connectivity.
/// Run with a test server or dev environment (not production).
final class Suite9_IntegrationE2ETests: AuthenticatedTestCase {
final class Suite9_IntegrationE2ETests: AuthenticatedUITestCase {
// Test user credentials - unique per test run
private let timestamp = Int(Date().timeIntervalSince1970)
override var needsAPISession: Bool { true }
private var userAUsername: String { "e2e_usera_\(timestamp)" }
private var userAEmail: String { "e2e_usera_\(timestamp)@test.com" }
private var userAPassword: String { "TestPass123!" }
// Unique ID for test data names
private let testRunId = Int(Date().timeIntervalSince1970)
private var userBUsername: String { "e2e_userb_\(timestamp)" }
private var userBEmail: String { "e2e_userb_\(timestamp)@test.com" }
private var userBPassword: String { "TestPass456!" }
/// Fixed verification code used by Go API when DEBUG=true
private let verificationCode = "123456"
// API-created test user for tests 02-07
private var apiUser: TestSession!
override func setUpWithError() throws {
// Create a unique test user via API (fast, reliable, no keyboard issues)
guard TestAccountAPIClient.isBackendReachable() else {
throw XCTSkip("Backend not reachable")
}
guard let user = TestAccountManager.createVerifiedAccount() else {
throw XCTSkip("Could not create test user via API")
}
apiUser = user
// Use the API-created user for UI login
_overrideCredentials = (user.username, user.password)
try super.setUpWithError()
}
override func tearDownWithError() throws {
try super.tearDownWithError()
private var _overrideCredentials: (String, String)?
override var testCredentials: (username: String, password: String) {
_overrideCredentials ?? ("testuser", "TestPass123!")
}
// MARK: - Helper Methods
private func ensureLoggedOut() {
UITestHelpers.ensureLoggedOut(app: app)
}
private func login(username: String, password: String) {
UITestHelpers.login(app: app, username: username, password: password)
}
/// Dismiss keyboard by tapping outside (doesn't submit forms)
private func dismissKeyboard() {
let coordinate = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.1))
coordinate.tap()
Thread.sleep(forTimeInterval: 0.5)
}
/// Dismiss strong password suggestion if shown
private func dismissStrongPasswordSuggestion() {
let chooseOwnPassword = app.buttons["Choose My Own Password"]
@@ -70,90 +63,40 @@ final class Suite9_IntegrationE2ETests: AuthenticatedTestCase {
// Mirrors TestIntegration_AuthenticationFlow
func test01_authenticationFlow() {
// Phase 1: Start on login screen
// This test verifies the full auth lifecycle via API
// (UI registration is tested by Suite1_RegistrationTests)
let timestamp = Int(Date().timeIntervalSince1970)
let testUser = "e2e_auth_\(testRunId)"
let testEmail = "e2e_auth_\(testRunId)@test.com"
let testPassword = "TestPass123!"
// Phase 1: Create user via API
guard let session = TestAccountAPIClient.createVerifiedAccount(
username: testUser, email: testEmail, password: testPassword
) else {
XCTFail("Could not create test user via API")
return
}
// Phase 2: Logout current user and login as new user via UI
UITestHelpers.ensureLoggedOut(app: app)
let welcomeText = app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
if !welcomeText.waitForExistence(timeout: 5) {
ensureLoggedOut()
}
XCTAssertTrue(welcomeText.waitForExistence(timeout: 10), "Should start on login screen")
XCTAssertTrue(welcomeText.waitForExistence(timeout: 10), "Should be on login screen")
UITestHelpers.login(app: app, username: testUser, password: testPassword)
// Phase 2: Navigate to registration
let signUpButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Sign Up'")).firstMatch
XCTAssertTrue(signUpButton.waitForExistence(timeout: 5), "Sign Up button should exist")
signUpButton.tap()
sleep(2)
// Phase 3: Fill registration form using proper accessibility identifiers
let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField]
XCTAssertTrue(usernameField.waitForExistence(timeout: 5), "Username field should exist")
usernameField.tap()
usernameField.typeText(userAUsername)
let emailField = app.textFields[AccessibilityIdentifiers.Authentication.registerEmailField]
XCTAssertTrue(emailField.waitForExistence(timeout: 3), "Email field should exist")
emailField.tap()
emailField.typeText(userAEmail)
// Password field - check both SecureField and TextField
var passwordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerPasswordField]
if !passwordField.exists {
passwordField = app.textFields[AccessibilityIdentifiers.Authentication.registerPasswordField]
}
XCTAssertTrue(passwordField.waitForExistence(timeout: 3), "Password field should exist")
passwordField.tap()
dismissStrongPasswordSuggestion()
passwordField.typeText(userAPassword)
// Confirm password field
var confirmPasswordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerConfirmPasswordField]
if !confirmPasswordField.exists {
confirmPasswordField = app.textFields[AccessibilityIdentifiers.Authentication.registerConfirmPasswordField]
}
XCTAssertTrue(confirmPasswordField.waitForExistence(timeout: 3), "Confirm password field should exist")
confirmPasswordField.tap()
dismissStrongPasswordSuggestion()
confirmPasswordField.typeText(userAPassword)
dismissKeyboard()
sleep(1)
// Phase 4: Submit registration
app.swipeUp()
sleep(1)
let registerButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton]
XCTAssertTrue(registerButton.waitForExistence(timeout: 5), "Register button should exist")
registerButton.tap()
sleep(3)
// Phase 5: Handle email verification
let verifyEmailTitle = app.staticTexts["Verify Your Email"]
XCTAssertTrue(verifyEmailTitle.waitForExistence(timeout: 10), "Verification screen must appear after registration")
sleep(3)
// Enter verification code - auto-submits when 6 digits entered
let codeField = app.textFields[AccessibilityIdentifiers.Authentication.verificationCodeField]
XCTAssertTrue(codeField.waitForExistence(timeout: 5), "Verification code field must exist")
codeField.tap()
codeField.typeText(verificationCode)
sleep(5)
// Phase 6: Verify logged in
// Phase 3: Verify logged in
let tabBar = app.tabBars.firstMatch
XCTAssertTrue(tabBar.waitForExistence(timeout: 15), "Should be logged in after registration")
XCTAssertTrue(tabBar.waitForExistence(timeout: 15), "Should be logged in after login")
// Phase 7: Logout
// Phase 4: Logout
UITestHelpers.logout(app: app)
// Phase 8: Login with created credentials
XCTAssertTrue(welcomeText.waitForExistence(timeout: 10), "Should be on login screen after logout")
login(username: userAUsername, password: userAPassword)
// Phase 9: Verify logged in
XCTAssertTrue(tabBar.waitForExistence(timeout: 10), "Should be logged in after login")
// Phase 5: Login again to verify re-login works
UITestHelpers.login(app: app, username: testUser, password: testPassword)
XCTAssertTrue(tabBar.waitForExistence(timeout: 10), "Should be logged in after re-login")
// Phase 10: Final logout
// Phase 6: Final logout
UITestHelpers.logout(app: app)
XCTAssertTrue(welcomeText.waitForExistence(timeout: 10), "Should be logged out")
}
@@ -163,76 +106,59 @@ final class Suite9_IntegrationE2ETests: AuthenticatedTestCase {
func test02_residenceCRUDFlow() {
// Ensure logged in as test user
UITestHelpers.ensureLoggedIn(app: app, username: userAUsername, password: userAPassword)
// Already logged in via setUp verify tab bar exists
XCTAssertTrue(app.tabBars.firstMatch.waitForExistence(timeout: defaultTimeout), "Should be logged in")
navigateToTab("Residences")
sleep(2)
let residenceName = "E2E Test Home \(timestamp)"
let residenceName = "E2E Test Home \(testRunId)"
// Phase 1: Create residence
let addButton = app.buttons[AccessibilityIdentifiers.Residence.addButton].firstMatch
XCTAssertTrue(addButton.waitForExistence(timeout: 5), "Add residence button should exist")
XCTAssertTrue(addButton.waitForExistence(timeout: defaultTimeout), "Add residence button should exist")
addButton.tap()
sleep(2)
// Fill form - just tap and type, don't dismiss keyboard between fields
// Fill form
let nameField = app.textFields[AccessibilityIdentifiers.Residence.nameField]
XCTAssertTrue(nameField.waitForExistence(timeout: 5), "Name field should exist")
nameField.tap()
sleep(1)
nameField.typeText(residenceName)
nameField.focusAndType(residenceName, app: app)
// Use return key to move to next field or dismiss, then scroll
app.keyboards.buttons["return"].tap()
sleep(1)
dismissKeyboard()
// Scroll to show more fields
app.swipeUp()
sleep(1)
// Fill street field
let streetField = app.textFields[AccessibilityIdentifiers.Residence.streetAddressField]
if streetField.waitForExistence(timeout: 3) && streetField.isHittable {
streetField.tap()
sleep(1)
streetField.typeText("123 E2E Test St")
app.keyboards.buttons["return"].tap()
sleep(1)
streetField.focusAndType("123 E2E Test St", app: app)
dismissKeyboard()
}
// Fill city field
let cityField = app.textFields[AccessibilityIdentifiers.Residence.cityField]
if cityField.waitForExistence(timeout: 3) && cityField.isHittable {
cityField.tap()
sleep(1)
cityField.typeText("Austin")
app.keyboards.buttons["return"].tap()
sleep(1)
cityField.focusAndType("Austin", app: app)
dismissKeyboard()
}
// Fill state field
let stateField = app.textFields[AccessibilityIdentifiers.Residence.stateProvinceField]
if stateField.waitForExistence(timeout: 3) && stateField.isHittable {
stateField.tap()
sleep(1)
stateField.typeText("TX")
app.keyboards.buttons["return"].tap()
sleep(1)
stateField.focusAndType("TX", app: app)
dismissKeyboard()
}
// Fill postal code field
let postalField = app.textFields[AccessibilityIdentifiers.Residence.postalCodeField]
if postalField.waitForExistence(timeout: 3) && postalField.isHittable {
postalField.tap()
sleep(1)
postalField.typeText("78701")
postalField.focusAndType("78701", app: app)
}
// Dismiss keyboard and scroll to save button
dismissKeyboard()
sleep(1)
_ = app.keyboards.firstMatch.waitForNonExistence(timeout: defaultTimeout)
app.swipeUp()
sleep(1)
// Save the residence
let saveButton = app.buttons[AccessibilityIdentifiers.Residence.saveButton]
@@ -240,15 +166,13 @@ final class Suite9_IntegrationE2ETests: AuthenticatedTestCase {
saveButton.tap()
} else {
// Try finding by label as fallback
let saveByLabel = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch
let saveByLabel = app.buttons[AccessibilityIdentifiers.Residence.saveButton].firstMatch
XCTAssertTrue(saveByLabel.waitForExistence(timeout: 5), "Save button should exist")
saveByLabel.tap()
}
sleep(3)
// Phase 2: Verify residence was created
navigateToTab("Residences")
sleep(2)
let residenceCard = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(residenceName)'")).firstMatch
XCTAssertTrue(residenceCard.waitForExistence(timeout: 10), "Residence '\(residenceName)' should appear in list")
}
@@ -258,24 +182,20 @@ final class Suite9_IntegrationE2ETests: AuthenticatedTestCase {
func test03_taskLifecycleFlow() {
// Ensure logged in
UITestHelpers.ensureLoggedIn(app: app, username: userAUsername, password: userAPassword)
// Already logged in via setUp verify tab bar exists
XCTAssertTrue(app.tabBars.firstMatch.waitForExistence(timeout: defaultTimeout), "Should be logged in")
// Ensure residence exists first - create one if empty
navigateToTab("Residences")
sleep(2)
let residenceCards = app.cells
if residenceCards.count == 0 {
// No residences, create one first
createMinimalResidence(name: "Task Test Home \(timestamp)")
sleep(2)
// Ensure residence exists (precondition for task creation)
if let residences = TestAccountAPIClient.listResidences(token: apiUser.token), residences.isEmpty {
TestDataSeeder.createResidence(token: apiUser.token, name: "Task Test Home \(testRunId)")
}
navigateToResidences()
pullToRefresh()
// Navigate to Tasks
navigateToTab("Tasks")
sleep(3)
let taskTitle = "E2E Task Lifecycle \(timestamp)"
let taskTitle = "E2E Task Lifecycle \(testRunId)"
// Phase 1: Create task - use firstMatch to avoid multiple element issue
let addButton = app.buttons[AccessibilityIdentifiers.Task.addButton].firstMatch
@@ -291,34 +211,28 @@ final class Suite9_IntegrationE2ETests: AuthenticatedTestCase {
}
addButton.tap()
sleep(2)
// Fill task form
let titleField = app.textFields[AccessibilityIdentifiers.Task.titleField].firstMatch
XCTAssertTrue(titleField.waitForExistence(timeout: 5), "Task title field should exist")
titleField.tap()
sleep(1)
titleField.typeText(taskTitle)
titleField.focusAndType(taskTitle, app: app)
dismissKeyboard()
sleep(1)
_ = app.keyboards.firstMatch.waitForNonExistence(timeout: defaultTimeout)
app.swipeUp()
sleep(1)
// Save the task
let saveTaskButton = app.buttons[AccessibilityIdentifiers.Task.saveButton].firstMatch
if saveTaskButton.waitForExistence(timeout: 5) && saveTaskButton.isHittable {
saveTaskButton.tap()
} else {
let saveByLabel = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save' OR label CONTAINS[c] 'Add Task' OR label CONTAINS[c] 'Create'")).firstMatch
let saveByLabel = app.buttons[AccessibilityIdentifiers.Task.saveButton].firstMatch
XCTAssertTrue(saveByLabel.exists, "Save/Create button should exist")
saveByLabel.tap()
}
sleep(3)
// Phase 2: Verify task was created
navigateToTab("Tasks")
sleep(2)
let taskCard = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(taskTitle)'")).firstMatch
XCTAssertTrue(taskCard.waitForExistence(timeout: 10), "Task '\(taskTitle)' should appear in task list")
}
@@ -327,9 +241,9 @@ final class Suite9_IntegrationE2ETests: AuthenticatedTestCase {
// Mirrors TestIntegration_TasksByResidenceKanban
func test04_kanbanColumnDistribution() {
UITestHelpers.ensureLoggedIn(app: app, username: userAUsername, password: userAPassword)
// Already logged in via setUp verify tab bar exists
XCTAssertTrue(app.tabBars.firstMatch.waitForExistence(timeout: defaultTimeout), "Should be logged in")
navigateToTab("Tasks")
sleep(3)
// Verify tasks screen is showing
let tasksTitle = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch
@@ -342,18 +256,17 @@ final class Suite9_IntegrationE2ETests: AuthenticatedTestCase {
// Mirrors TestIntegration_CrossUserAccessDenied
func test05_crossUserAccessControl() {
UITestHelpers.ensureLoggedIn(app: app, username: userAUsername, password: userAPassword)
// Already logged in via setUp verify tab bar exists
XCTAssertTrue(app.tabBars.firstMatch.waitForExistence(timeout: defaultTimeout), "Should be logged in")
// Verify user can access their residences tab
navigateToTab("Residences")
sleep(2)
let residencesVisible = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch.isSelected
XCTAssertTrue(residencesVisible, "User should be able to access Residences tab")
// Verify user can access their tasks tab
navigateToTab("Tasks")
sleep(2)
let tasksAccessible = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch.isSelected
XCTAssertTrue(tasksAccessible, "User should be able to access Tasks tab")
@@ -363,49 +276,37 @@ final class Suite9_IntegrationE2ETests: AuthenticatedTestCase {
// Mirrors TestIntegration_LookupEndpoints
func test06_lookupDataAvailable() {
UITestHelpers.ensureLoggedIn(app: app, username: userAUsername, password: userAPassword)
// Already logged in via setUp verify tab bar exists
XCTAssertTrue(app.tabBars.firstMatch.waitForExistence(timeout: defaultTimeout), "Should be logged in")
// Navigate to add residence to check residence types are loaded
navigateToTab("Residences")
sleep(2)
let addButton = app.buttons[AccessibilityIdentifiers.Residence.addButton].firstMatch
if addButton.waitForExistence(timeout: 5) {
addButton.tap()
sleep(2)
addButton.waitForExistenceOrFail(timeout: navigationTimeout, message: "Add residence button should exist")
addButton.tap()
// Check property type picker exists (indicates lookups loaded)
let propertyTypePicker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Property Type' OR label CONTAINS[c] 'Type'")).firstMatch
let pickerExists = propertyTypePicker.exists
// Check property type picker exists (indicates lookups loaded)
let propertyTypePicker = app.buttons[AccessibilityIdentifiers.Residence.propertyTypePicker].firstMatch
XCTAssertTrue(propertyTypePicker.waitForExistence(timeout: navigationTimeout), "Property type picker should exist (lookups loaded)")
// Cancel form
let cancelButton = app.buttons[AccessibilityIdentifiers.Residence.formCancelButton]
if cancelButton.exists {
cancelButton.tap()
} else {
let cancelByLabel = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Cancel'")).firstMatch
if cancelByLabel.exists {
cancelByLabel.tap()
}
}
XCTAssertTrue(pickerExists, "Property type picker should exist (lookups loaded)")
}
// Cancel form
let cancelButton = app.buttons[AccessibilityIdentifiers.Residence.formCancelButton].firstMatch
if cancelButton.exists { cancelButton.tap() }
}
// MARK: - Test 7: Residence Sharing Flow
// Mirrors TestIntegration_ResidenceSharingFlow
func test07_residenceSharingUIElements() {
UITestHelpers.ensureLoggedIn(app: app, username: userAUsername, password: userAPassword)
// Already logged in via setUp verify tab bar exists
XCTAssertTrue(app.tabBars.firstMatch.waitForExistence(timeout: defaultTimeout), "Should be logged in")
navigateToTab("Residences")
sleep(2)
// Find any residence to check sharing UI
let residenceCard = app.cells.firstMatch
if residenceCard.waitForExistence(timeout: 5) {
if residenceCard.waitForExistence(timeout: defaultTimeout) {
residenceCard.tap()
sleep(2)
// Look for share button in residence details
let shareButton = app.buttons[AccessibilityIdentifiers.Residence.shareButton]
@@ -418,7 +319,6 @@ final class Suite9_IntegrationE2ETests: AuthenticatedTestCase {
let backButton = app.navigationBars.buttons.element(boundBy: 0)
if backButton.exists && backButton.isHittable {
backButton.tap()
sleep(1)
}
}
}
@@ -430,76 +330,55 @@ final class Suite9_IntegrationE2ETests: AuthenticatedTestCase {
guard addButton.waitForExistence(timeout: 5) else { return }
addButton.tap()
sleep(2)
// Fill name field
let nameField = app.textFields[AccessibilityIdentifiers.Residence.nameField]
if nameField.waitForExistence(timeout: 5) {
nameField.tap()
sleep(1)
nameField.typeText(name)
app.keyboards.buttons["return"].tap()
sleep(1)
if nameField.waitForExistence(timeout: defaultTimeout) {
nameField.focusAndType(name, app: app)
dismissKeyboard()
}
// Scroll to show address fields
app.swipeUp()
sleep(1)
// Fill street field
let streetField = app.textFields[AccessibilityIdentifiers.Residence.streetAddressField]
if streetField.waitForExistence(timeout: 3) && streetField.isHittable {
streetField.tap()
sleep(1)
streetField.typeText("123 Test St")
app.keyboards.buttons["return"].tap()
sleep(1)
streetField.focusAndType("123 Test St", app: app)
dismissKeyboard()
}
// Fill city field
let cityField = app.textFields[AccessibilityIdentifiers.Residence.cityField]
if cityField.waitForExistence(timeout: 3) && cityField.isHittable {
cityField.tap()
sleep(1)
cityField.typeText("Austin")
app.keyboards.buttons["return"].tap()
sleep(1)
cityField.focusAndType("Austin", app: app)
dismissKeyboard()
}
// Fill state field
let stateField = app.textFields[AccessibilityIdentifiers.Residence.stateProvinceField]
if stateField.waitForExistence(timeout: 3) && stateField.isHittable {
stateField.tap()
sleep(1)
stateField.typeText("TX")
app.keyboards.buttons["return"].tap()
sleep(1)
stateField.focusAndType("TX", app: app)
dismissKeyboard()
}
// Fill postal code field
let postalField = app.textFields[AccessibilityIdentifiers.Residence.postalCodeField]
if postalField.waitForExistence(timeout: 3) && postalField.isHittable {
postalField.tap()
sleep(1)
postalField.typeText("78701")
postalField.focusAndType("78701", app: app)
}
dismissKeyboard()
sleep(1)
_ = app.keyboards.firstMatch.waitForNonExistence(timeout: defaultTimeout)
app.swipeUp()
sleep(1)
// Save
let saveButton = app.buttons[AccessibilityIdentifiers.Residence.saveButton]
let saveButton = app.buttons[AccessibilityIdentifiers.Residence.saveButton].firstMatch
if saveButton.waitForExistence(timeout: 5) && saveButton.isHittable {
saveButton.tap()
} else {
let saveByLabel = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch
if saveByLabel.exists {
saveByLabel.tap()
}
}
sleep(3)
// Wait for save to complete and return to list
_ = app.cells.firstMatch.waitForExistence(timeout: defaultTimeout)
}
// MARK: - Helper: Find Add Task Button