Files
honeyDueKMP/iosApp/CaseraUITests/Tests/StabilityTests.swift
treyt fc0e0688eb Add comprehensive iOS unit and UI test suites for greenfield test plan
- Create unit tests: DataLayerTests (27 tests for DATA-001–007), DataManagerExtendedTests
  (20 tests for TASK-005, TASK-012, TCOMP-003, THEME-001, QA-002), plus ValidationHelpers,
  TaskMetrics, StringExtensions, DoubleExtensions, DateUtils, DocumentHelpers, ErrorMessageParser
- Create UI tests: AuthenticationTests, PasswordResetTests, OnboardingTests, TaskIntegration,
  ContractorIntegration, ResidenceIntegration, DocumentIntegration, DataLayer, Stability
- Add UI test framework: AuthenticatedTestCase, ScreenObjects, TestFlows, TestAccountManager,
  TestAccountAPIClient, TestDataCleaner, TestDataSeeder
- Add accessibility identifiers to password reset views for UI test support
- Add greenfield test plan CSVs and update automated column for 27 test IDs
- All 297 unit tests pass across 60 suites

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 15:37:56 -06:00

166 lines
6.9 KiB
Swift

import XCTest
final class StabilityTests: BaseUITestCase {
func testP001_RapidOnboardingNavigationDoesNotCrash() {
for _ in 0..<3 {
let welcome = OnboardingWelcomeScreen(app: app)
welcome.waitForLoad(timeout: defaultTimeout)
welcome.tapStartFresh()
let valueProps = OnboardingValuePropsScreen(app: app)
valueProps.waitForLoad(timeout: defaultTimeout)
valueProps.tapBack()
welcome.waitForLoad(timeout: defaultTimeout)
}
}
func testP002_RepeatedForwardNavigationRemainsResponsive() {
for index in 0..<3 {
let welcome = OnboardingWelcomeScreen(app: app)
welcome.waitForLoad(timeout: defaultTimeout)
welcome.tapStartFresh()
let valueProps = OnboardingValuePropsScreen(app: app)
valueProps.waitForLoad(timeout: defaultTimeout)
valueProps.tapContinue()
let nameResidence = OnboardingNameResidenceScreen(app: app)
nameResidence.waitForLoad(timeout: defaultTimeout)
nameResidence.enterResidenceName("Stress Home \(index)")
nameResidence.tapBack()
valueProps.waitForLoad(timeout: defaultTimeout)
valueProps.tapBack()
welcome.waitForLoad(timeout: defaultTimeout)
}
}
func testP003_RapidDoubleTapOnValuePropsContinueLandsOnNameResidence() {
let welcome = OnboardingWelcomeScreen(app: app)
welcome.waitForLoad(timeout: defaultTimeout)
welcome.tapStartFresh()
let valueProps = OnboardingValuePropsScreen(app: app)
valueProps.waitForLoad(timeout: defaultTimeout)
let continueButton = app.buttons[UITestID.Onboarding.valuePropsNextButton]
continueButton.waitUntilHittable(timeout: defaultTimeout).tap()
if continueButton.exists && continueButton.isHittable {
continueButton.tap()
}
let nameResidence = OnboardingNameResidenceScreen(app: app)
nameResidence.waitForLoad(timeout: defaultTimeout)
}
// MARK: - Additional Stability Coverage
func testP004_StartFreshThenBackToWelcomeThenJoinExistingDoesNotCorruptState() {
let welcome = OnboardingWelcomeScreen(app: app)
welcome.waitForLoad(timeout: defaultTimeout)
// Start fresh path
welcome.tapStartFresh()
let valueProps = OnboardingValuePropsScreen(app: app)
valueProps.waitForLoad(timeout: defaultTimeout)
// Go back to welcome
valueProps.tapBack()
welcome.waitForLoad(timeout: defaultTimeout)
// Switch to join existing path
welcome.tapJoinExisting()
let createAccount = OnboardingCreateAccountScreen(app: app)
createAccount.waitForLoad(timeout: defaultTimeout)
}
func testP005_RepeatedLoginNavigationRemainsStable() {
for _ in 0..<3 {
let login = TestFlows.navigateToLoginFromOnboarding(app: app)
login.waitForLoad(timeout: defaultTimeout)
// Dismiss login (swipe down or navigate back)
let backButton = app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.backButton).firstMatch
if backButton.waitForExistence(timeout: shortTimeout) && backButton.isHittable {
backButton.forceTap()
} else {
// Try swipe down to dismiss sheet
app.swipeDown()
}
// Should return to onboarding
let welcome = OnboardingWelcomeScreen(app: app)
welcome.waitForLoad(timeout: defaultTimeout)
}
}
// MARK: - OFF-003: Retry Button Existence
/// OFF-003: Retry button is accessible from error states.
///
/// A true end-to-end retry test (where the network actually fails then succeeds)
/// is not feasible in XCUITest without network manipulation infrastructure. This
/// test verifies the structural requirement: that the retry accessibility identifier
/// `AccessibilityIdentifiers.Common.retryButton` is defined and that any error view
/// in the app exposes a tappable retry control.
///
/// When an error view IS visible (e.g., backend is unreachable), the test asserts the
/// retry button exists and can be tapped without crashing the app.
func testP010_retryButtonExistsOnErrorState() {
// Navigate to the login screen from onboarding this is the most common
// path that could encounter an error state if the backend is unreachable.
let welcome = OnboardingWelcomeScreen(app: app)
welcome.waitForLoad(timeout: defaultTimeout)
welcome.tapAlreadyHaveAccount()
let login = LoginScreen(app: app)
login.waitForLoad(timeout: defaultTimeout)
// Attempt login with intentionally wrong credentials to trigger an error state
login.enterUsername("nonexistent_user_off003")
login.enterPassword("WrongPass!")
let loginButton = app.buttons[UITestID.Auth.loginButton]
loginButton.waitUntilHittable(timeout: defaultTimeout).tap()
// Wait briefly to allow any error state to appear
sleep(3)
// Check for error view and retry button
let retryButton = app.buttons[AccessibilityIdentifiers.Common.retryButton]
let errorView = app.otherElements[AccessibilityIdentifiers.Common.errorView]
// If an error view is visible, assert the retry button is also present and tappable
if errorView.exists {
XCTAssertTrue(
retryButton.waitForExistence(timeout: shortTimeout),
"Retry button (\(AccessibilityIdentifiers.Common.retryButton)) should exist when an error view is shown"
)
XCTAssertTrue(
retryButton.isEnabled,
"Retry button should be enabled so the user can re-attempt the failed operation"
)
// Tapping retry should not crash the app
retryButton.forceTap()
sleep(1)
XCTAssertTrue(app.exists, "App should remain running after tapping retry")
} else {
// No error view is currently visible this is acceptable if login
// shows an inline error message instead. Confirm the app is still in a
// usable state (it did not crash and the login screen is still present).
let stillOnLogin = app.textFields[UITestID.Auth.usernameField].exists
let showsAlert = app.alerts.firstMatch.exists
let showsErrorText = app.staticTexts.containing(
NSPredicate(format: "label CONTAINS[c] 'invalid' OR label CONTAINS[c] 'incorrect' OR label CONTAINS[c] 'error'")
).firstMatch.exists
XCTAssertTrue(
stillOnLogin || showsAlert || showsErrorText,
"After a failed login the app should show an error state — login screen, alert, or inline error"
)
}
}
}