- 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>
166 lines
6.9 KiB
Swift
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"
|
|
)
|
|
}
|
|
}
|
|
}
|