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

@@ -4,23 +4,32 @@ import XCTest
///
/// Test Plan IDs: AUTH-015, AUTH-016, AUTH-017
final class PasswordResetTests: BaseUITestCase {
override var relaunchBetweenTests: Bool { true }
private var testSession: TestSession?
private var cleaner: TestDataCleaner?
override func setUpWithError() throws {
guard TestAccountAPIClient.isBackendReachable() else {
throw XCTSkip("Local backend is not reachable at \(TestAccountAPIClient.baseURL)")
}
// Create a verified account via API so we have real credentials for reset
guard let session = TestAccountManager.createVerifiedAccount() else {
throw XCTSkip("Could not create verified test account")
}
testSession = session
cleaner = TestDataCleaner(token: session.token)
// Force clean app launch password reset flow leaves complex screen state
app.terminate()
try super.setUpWithError()
}
override func tearDownWithError() throws {
cleaner?.cleanAll()
try super.tearDownWithError()
}
// MARK: - AUTH-015: Verify reset code reaches new password screen
func testAUTH015_VerifyResetCodeSuccessPath() throws {
@@ -44,7 +53,7 @@ final class PasswordResetTests: BaseUITestCase {
// Should reach the new password screen
let resetScreen = ResetPasswordScreen(app: app)
resetScreen.waitForLoad(timeout: longTimeout)
try resetScreen.waitForLoad(timeout: loginTimeout)
}
// MARK: - AUTH-016: Full reset password cycle + login with new password
@@ -58,46 +67,52 @@ final class PasswordResetTests: BaseUITestCase {
login.tapForgotPassword()
// Complete the full reset flow via UI
TestFlows.completeForgotPasswordFlow(
try TestFlows.completeForgotPasswordFlow(
app: app,
email: session.user.email,
newPassword: newPassword
)
// Wait for success indication - either success message or return to login
let successText = app.staticTexts.containing(
NSPredicate(format: "label CONTAINS[c] 'success' OR label CONTAINS[c] 'reset'")
).firstMatch
// After reset, the app auto-logs in with the new password.
// If auto-login succeeds app goes directly to main tabs (sheet dismissed).
// If auto-login fails success message + "Return to Login" button appear.
let tabBar = app.tabBars.firstMatch
let returnButton = app.buttons[UITestID.PasswordReset.returnToLoginButton]
let deadline = Date().addingTimeInterval(longTimeout)
var succeeded = false
let deadline = Date().addingTimeInterval(loginTimeout)
var reachedPostReset = false
while Date() < deadline {
if successText.exists || returnButton.exists {
succeeded = true
if tabBar.exists {
// Auto-login succeeded password reset worked!
reachedPostReset = true
break
}
if returnButton.exists {
// Auto-login failed manual login needed
reachedPostReset = true
returnButton.forceTap()
break
}
RunLoop.current.run(until: Date().addingTimeInterval(0.5))
}
XCTAssertTrue(succeeded, "Expected success indication after password reset")
XCTAssertTrue(reachedPostReset, "Expected main tabs (auto-login) or return button (manual login) after password reset")
// If return to login button appears, tap it
if returnButton.exists && returnButton.isHittable {
returnButton.tap()
if tabBar.exists {
// Already logged in via auto-login test passed
return
}
// Verify we can login with the new password through the UI
// Manual login path: return button was tapped, now on login screen
let loginScreen = LoginScreenObject(app: app)
loginScreen.waitForLoad()
loginScreen.waitForLoad(timeout: loginTimeout)
loginScreen.enterUsername(session.username)
loginScreen.enterPassword(newPassword)
let loginButton = app.buttons[UITestID.Auth.loginButton]
loginButton.waitUntilHittable(timeout: 10).tap()
let tabBar = app.tabBars.firstMatch
XCTAssertTrue(tabBar.waitForExistence(timeout: 15), "Should login successfully with new password")
XCTAssertTrue(tabBar.waitForExistence(timeout: loginTimeout), "Should login successfully with new password")
}
// MARK: - AUTH-015 (alias): Verify reset code reaches the new password screen
@@ -125,7 +140,7 @@ final class PasswordResetTests: BaseUITestCase {
// The reset password screen should now appear
let resetScreen = ResetPasswordScreen(app: app)
resetScreen.waitForLoad(timeout: longTimeout)
try resetScreen.waitForLoad(timeout: loginTimeout)
}
// MARK: - AUTH-016 (alias): Full reset flow + login with new password
@@ -140,7 +155,7 @@ final class PasswordResetTests: BaseUITestCase {
let login = TestFlows.navigateToLoginFromOnboarding(app: app)
login.tapForgotPassword()
TestFlows.completeForgotPasswordFlow(
try TestFlows.completeForgotPasswordFlow(
app: app,
email: session.user.email,
newPassword: newPassword
@@ -152,34 +167,39 @@ final class PasswordResetTests: BaseUITestCase {
).firstMatch
let returnButton = app.buttons[UITestID.PasswordReset.returnToLoginButton]
let deadline = Date().addingTimeInterval(longTimeout)
var resetSucceeded = false
// After reset, the app auto-logs in with the new password.
// If auto-login succeeds app goes to main tabs. If fails return button appears.
let tabBar = app.tabBars.firstMatch
let deadline = Date().addingTimeInterval(loginTimeout)
var reachedPostReset = false
while Date() < deadline {
if successText.exists || returnButton.exists {
resetSucceeded = true
if tabBar.exists {
reachedPostReset = true
break
}
if returnButton.exists {
reachedPostReset = true
returnButton.forceTap()
break
}
RunLoop.current.run(until: Date().addingTimeInterval(0.5))
}
XCTAssertTrue(resetSucceeded, "Expected success indication after password reset")
XCTAssertTrue(reachedPostReset, "Expected main tabs (auto-login) or return button after password reset")
// If the return-to-login button is present, tap it to go back to the login screen
if returnButton.exists && returnButton.isHittable {
returnButton.tap()
}
if tabBar.exists { return }
// Confirm the new password works by logging in through the UI
// Manual login fallback
let loginScreen = LoginScreenObject(app: app)
loginScreen.waitForLoad()
loginScreen.waitForLoad(timeout: loginTimeout)
loginScreen.enterUsername(session.username)
loginScreen.enterPassword(newPassword)
let loginButton = app.buttons[UITestID.Auth.loginButton]
loginButton.waitUntilHittable(timeout: 10).tap()
let tabBar = app.tabBars.firstMatch
XCTAssertTrue(tabBar.waitForExistence(timeout: 15), "Should login successfully with new password")
XCTAssertTrue(tabBar.waitForExistence(timeout: loginTimeout), "Should login successfully with new password")
}
// MARK: - AUTH-017: Mismatched passwords are blocked
@@ -204,7 +224,7 @@ final class PasswordResetTests: BaseUITestCase {
// Enter mismatched passwords
let resetScreen = ResetPasswordScreen(app: app)
resetScreen.waitForLoad(timeout: longTimeout)
try resetScreen.waitForLoad(timeout: loginTimeout)
resetScreen.enterNewPassword("ValidPass123!")
resetScreen.enterConfirmPassword("DifferentPass456!")