Files
honeyDueKMP/iosApp/HoneyDueUITests/Tests/PasswordResetTests.swift
Trey T 4df8707b92 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>
2026-03-23 15:05:37 -05:00

235 lines
8.8 KiB
Swift

import XCTest
/// Tests for the password reset flow against the local backend (DEBUG=true, code=123456).
///
/// 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)")
}
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 {
let session = try XCTUnwrap(testSession)
// Navigate to forgot password
let login = TestFlows.navigateToLoginFromOnboarding(app: app)
login.tapForgotPassword()
// Enter email and send code
let forgotScreen = ForgotPasswordScreen(app: app)
forgotScreen.waitForLoad()
forgotScreen.enterEmail(session.user.email)
forgotScreen.tapSendCode()
// Enter the debug verification code
let verifyScreen = VerifyResetCodeScreen(app: app)
verifyScreen.waitForLoad()
verifyScreen.enterCode(TestAccountAPIClient.debugVerificationCode)
verifyScreen.tapVerify()
// Should reach the new password screen
let resetScreen = ResetPasswordScreen(app: app)
try resetScreen.waitForLoad(timeout: loginTimeout)
}
// MARK: - AUTH-016: Full reset password cycle + login with new password
func testAUTH016_ResetPasswordSuccess() throws {
let session = try XCTUnwrap(testSession)
let newPassword = "NewPass9876!"
// Navigate to forgot password
let login = TestFlows.navigateToLoginFromOnboarding(app: app)
login.tapForgotPassword()
// Complete the full reset flow via UI
try TestFlows.completeForgotPasswordFlow(
app: app,
email: session.user.email,
newPassword: newPassword
)
// 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(loginTimeout)
var reachedPostReset = false
while Date() < deadline {
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(reachedPostReset, "Expected main tabs (auto-login) or return button (manual login) after password reset")
if tabBar.exists {
// Already logged in via auto-login test passed
return
}
// Manual login path: return button was tapped, now on login screen
let loginScreen = LoginScreenObject(app: app)
loginScreen.waitForLoad(timeout: loginTimeout)
loginScreen.enterUsername(session.username)
loginScreen.enterPassword(newPassword)
let loginButton = app.buttons[UITestID.Auth.loginButton]
loginButton.waitUntilHittable(timeout: 10).tap()
XCTAssertTrue(tabBar.waitForExistence(timeout: loginTimeout), "Should login successfully with new password")
}
// MARK: - AUTH-015 (alias): Verify reset code reaches the new password screen
func test03_verifyResetCodeSuccess() throws {
try XCTSkipIf(!TestAccountAPIClient.isBackendReachable(), "Backend not reachable")
let session = try XCTUnwrap(testSession)
// Navigate to forgot password
let login = TestFlows.navigateToLoginFromOnboarding(app: app)
login.tapForgotPassword()
// Enter email and send the reset code
let forgotScreen = ForgotPasswordScreen(app: app)
forgotScreen.waitForLoad()
forgotScreen.enterEmail(session.user.email)
forgotScreen.tapSendCode()
// Enter the debug verification code on the verify screen
let verifyScreen = VerifyResetCodeScreen(app: app)
verifyScreen.waitForLoad()
verifyScreen.enterCode(TestAccountAPIClient.debugVerificationCode)
verifyScreen.tapVerify()
// The reset password screen should now appear
let resetScreen = ResetPasswordScreen(app: app)
try resetScreen.waitForLoad(timeout: loginTimeout)
}
// MARK: - AUTH-016 (alias): Full reset flow + login with new password
func test04_resetPasswordSuccessAndLogin() throws {
try XCTSkipIf(!TestAccountAPIClient.isBackendReachable(), "Backend not reachable")
let session = try XCTUnwrap(testSession)
let newPassword = "NewPass9876!"
// Navigate to forgot password, then drive the complete 3-step reset flow
let login = TestFlows.navigateToLoginFromOnboarding(app: app)
login.tapForgotPassword()
try TestFlows.completeForgotPasswordFlow(
app: app,
email: session.user.email,
newPassword: newPassword
)
// Wait for a success indication either a success message or the return-to-login button
let successText = app.staticTexts.containing(
NSPredicate(format: "label CONTAINS[c] 'success' OR label CONTAINS[c] 'reset'")
).firstMatch
let returnButton = app.buttons[UITestID.PasswordReset.returnToLoginButton]
// 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 tabBar.exists {
reachedPostReset = true
break
}
if returnButton.exists {
reachedPostReset = true
returnButton.forceTap()
break
}
RunLoop.current.run(until: Date().addingTimeInterval(0.5))
}
XCTAssertTrue(reachedPostReset, "Expected main tabs (auto-login) or return button after password reset")
if tabBar.exists { return }
// Manual login fallback
let loginScreen = LoginScreenObject(app: app)
loginScreen.waitForLoad(timeout: loginTimeout)
loginScreen.enterUsername(session.username)
loginScreen.enterPassword(newPassword)
let loginButton = app.buttons[UITestID.Auth.loginButton]
loginButton.waitUntilHittable(timeout: 10).tap()
XCTAssertTrue(tabBar.waitForExistence(timeout: loginTimeout), "Should login successfully with new password")
}
// MARK: - AUTH-017: Mismatched passwords are blocked
func testAUTH017_MismatchedPasswordBlocked() throws {
let session = try XCTUnwrap(testSession)
// Navigate to forgot password
let login = TestFlows.navigateToLoginFromOnboarding(app: app)
login.tapForgotPassword()
// Get to the reset password screen
let forgotScreen = ForgotPasswordScreen(app: app)
forgotScreen.waitForLoad()
forgotScreen.enterEmail(session.user.email)
forgotScreen.tapSendCode()
let verifyScreen = VerifyResetCodeScreen(app: app)
verifyScreen.waitForLoad()
verifyScreen.enterCode(TestAccountAPIClient.debugVerificationCode)
verifyScreen.tapVerify()
// Enter mismatched passwords
let resetScreen = ResetPasswordScreen(app: app)
try resetScreen.waitForLoad(timeout: loginTimeout)
resetScreen.enterNewPassword("ValidPass123!")
resetScreen.enterConfirmPassword("DifferentPass456!")
// The reset button should be disabled when passwords don't match
XCTAssertFalse(resetScreen.isResetButtonEnabled, "Reset button should be disabled when passwords don't match")
}
}