Files
honeyDueKMP/iosApp/CaseraUITests/Tests/OnboardingTests.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

258 lines
12 KiB
Swift

import XCTest
final class OnboardingTests: BaseUITestCase {
func testF101_StartFreshFlowReachesCreateAccount() {
let createAccount = TestFlows.navigateStartFreshToCreateAccount(app: app, residenceName: "Blueprint House")
createAccount.waitForLoad(timeout: defaultTimeout)
}
func testF102_JoinExistingFlowGoesToCreateAccount() {
let welcome = OnboardingWelcomeScreen(app: app)
welcome.waitForLoad()
welcome.tapJoinExisting()
let createAccount = OnboardingCreateAccountScreen(app: app)
createAccount.waitForLoad(timeout: defaultTimeout)
}
func testF103_BackNavigationFromNameResidenceReturnsToValueProps() {
let welcome = OnboardingWelcomeScreen(app: app)
welcome.waitForLoad()
welcome.tapStartFresh()
let valueProps = OnboardingValuePropsScreen(app: app)
valueProps.waitForLoad()
valueProps.tapContinue()
let nameResidence = OnboardingNameResidenceScreen(app: app)
nameResidence.waitForLoad()
nameResidence.tapBack()
XCTAssertTrue(app.otherElements[UITestID.Root.onboarding].waitForExistence(timeout: defaultTimeout))
}
func testF104_SkipOnValuePropsMovesToNameResidence() {
let welcome = OnboardingWelcomeScreen(app: app)
welcome.waitForLoad()
welcome.tapStartFresh()
let valueProps = OnboardingValuePropsScreen(app: app)
valueProps.waitForLoad()
let skipButton = app.buttons[UITestID.Onboarding.skipButton]
skipButton.waitForExistenceOrFail(timeout: defaultTimeout)
skipButton.forceTap()
let nameResidence = OnboardingNameResidenceScreen(app: app)
nameResidence.waitForLoad(timeout: defaultTimeout)
}
// MARK: - Additional Onboarding Coverage
func testF105_JoinExistingFlowSkipsValuePropsAndNameResidence() {
let welcome = OnboardingWelcomeScreen(app: app)
welcome.waitForLoad()
welcome.tapJoinExisting()
let createAccount = OnboardingCreateAccountScreen(app: app)
createAccount.waitForLoad(timeout: defaultTimeout)
// Verify value props and name residence screens were NOT shown
let valuePropsTitle = app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.valuePropsContainer).firstMatch
XCTAssertFalse(valuePropsTitle.exists, "Value props should be skipped for Join Existing flow")
let nameResidenceTitle = app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.nameResidenceTitle).firstMatch
XCTAssertFalse(nameResidenceTitle.exists, "Name residence should be skipped for Join Existing flow")
}
func testF106_NameResidenceFieldAcceptsInput() {
let welcome = OnboardingWelcomeScreen(app: app)
welcome.waitForLoad()
welcome.tapStartFresh()
let valueProps = OnboardingValuePropsScreen(app: app)
valueProps.waitForLoad()
valueProps.tapContinue()
let nameResidence = OnboardingNameResidenceScreen(app: app)
nameResidence.waitForLoad()
let nameField = app.textFields[UITestID.Onboarding.residenceNameField]
nameField.waitUntilHittable(timeout: defaultTimeout).tap()
nameField.typeText("My Test Home")
XCTAssertEqual(nameField.value as? String, "My Test Home", "Residence name field should accept and display typed text")
}
func testF107_ProgressIndicatorVisibleDuringOnboarding() {
let welcome = OnboardingWelcomeScreen(app: app)
welcome.waitForLoad()
welcome.tapStartFresh()
let valueProps = OnboardingValuePropsScreen(app: app)
valueProps.waitForLoad()
let progress = app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.progressIndicator).firstMatch
XCTAssertTrue(progress.waitForExistence(timeout: defaultTimeout), "Progress indicator should be visible during onboarding flow")
}
func testF108_BackFromCreateAccountNavigatesToPreviousStep() {
let createAccount = TestFlows.navigateStartFreshToCreateAccount(app: app, residenceName: "Back Test")
createAccount.waitForLoad(timeout: defaultTimeout)
let backButton = app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.backButton).firstMatch
backButton.waitForExistenceOrFail(timeout: defaultTimeout)
backButton.forceTap()
// Should return to name residence step
let nameResidence = OnboardingNameResidenceScreen(app: app)
nameResidence.waitForLoad(timeout: defaultTimeout)
}
// MARK: - ONB-005: Residence Bootstrap
/// ONB-005: Start Fresh creates a residence automatically after email verification.
/// Drives the full Start Fresh flow welcome value props name residence
/// create account verify email then confirms the app lands on main tabs,
/// which indicates the residence was bootstrapped during onboarding.
func testF110_startFreshCreatesResidenceAfterVerification() {
try? XCTSkipIf(
!TestAccountAPIClient.isBackendReachable(),
"Local backend is not reachable — skipping ONB-005"
)
// Generate unique credentials so we don't collide with other test runs
let creds = TestAccountManager.uniqueCredentials(prefix: "onb005")
let uniqueResidenceName = "ONB005 Home \(Int(Date().timeIntervalSince1970))"
// Step 1: Navigate Start Fresh flow to the Create Account screen
let createAccount = TestFlows.navigateStartFreshToCreateAccount(app: app, residenceName: uniqueResidenceName)
createAccount.waitForLoad(timeout: defaultTimeout)
// Step 2: Expand the email sign-up form and fill it in
createAccount.expandEmailSignup()
// Use the Onboarding-specific field identifiers for the create account form
let onbUsernameField = app.textFields[AccessibilityIdentifiers.Onboarding.usernameField]
let onbEmailField = app.textFields[AccessibilityIdentifiers.Onboarding.emailField]
let onbPasswordField = app.secureTextFields[AccessibilityIdentifiers.Onboarding.passwordField]
let onbConfirmPasswordField = app.secureTextFields[AccessibilityIdentifiers.Onboarding.confirmPasswordField]
onbUsernameField.waitForExistenceOrFail(timeout: defaultTimeout)
onbUsernameField.forceTap()
onbUsernameField.typeText(creds.username)
onbEmailField.waitForExistenceOrFail(timeout: defaultTimeout)
onbEmailField.forceTap()
onbEmailField.typeText(creds.email)
onbPasswordField.waitForExistenceOrFail(timeout: defaultTimeout)
onbPasswordField.forceTap()
onbPasswordField.typeText(creds.password)
onbConfirmPasswordField.waitForExistenceOrFail(timeout: defaultTimeout)
onbConfirmPasswordField.forceTap()
onbConfirmPasswordField.typeText(creds.password)
// Step 3: Submit the create account form
let createAccountButton = app.descendants(matching: .any)
.matching(identifier: UITestID.Onboarding.createAccountButton).firstMatch
createAccountButton.waitForExistenceOrFail(timeout: defaultTimeout)
createAccountButton.forceTap()
// Step 4: Verify email with the debug code
let verificationScreen = VerificationScreen(app: app)
verificationScreen.waitForLoad(timeout: longTimeout)
verificationScreen.enterCode(TestAccountAPIClient.debugVerificationCode)
verificationScreen.submitCode()
// Step 5: After verification, the app should transition to main tabs.
// Landing on main tabs proves the onboarding completed and the residence
// was bootstrapped automatically no manual residence creation was required.
let mainTabs = app.otherElements[UITestID.Root.mainTabs]
let tabBar = app.tabBars.firstMatch
let reachedMain = mainTabs.waitForExistence(timeout: longTimeout)
|| tabBar.waitForExistence(timeout: 5)
XCTAssertTrue(
reachedMain,
"App should reach main tabs after Start Fresh onboarding + email verification, " +
"confirming the residence '\(uniqueResidenceName)' was created automatically"
)
}
// MARK: - ONB-008: Completion Persistence
/// ONB-008: Completing onboarding persists the completion flag so the next
/// launch bypasses onboarding entirely and goes directly to login or main tabs.
func testF111_completedOnboardingBypassedOnRelaunch() {
try? XCTSkipIf(
!TestAccountAPIClient.isBackendReachable(),
"Local backend is not reachable — skipping ONB-008"
)
// Step 1: Complete onboarding via the Join Existing path (quickest path to main tabs).
// Navigate to the create account screen which marks the onboarding intent as started.
// Then use a pre-seeded account so we can reach main tabs without creating a new account.
let welcome = OnboardingWelcomeScreen(app: app)
welcome.waitForLoad()
welcome.tapAlreadyHaveAccount()
// Log in with the seeded account to complete onboarding and reach main tabs
let login = LoginScreen(app: app)
login.waitForLoad(timeout: defaultTimeout)
login.enterUsername("admin")
login.enterPassword("test1234")
let loginButton = app.buttons[UITestID.Auth.loginButton]
loginButton.waitUntilHittable(timeout: defaultTimeout).tap()
// Wait for main tabs this confirms onboarding is considered complete
let mainTabs = app.otherElements[UITestID.Root.mainTabs]
let tabBar = app.tabBars.firstMatch
let reachedMain = mainTabs.waitForExistence(timeout: longTimeout)
|| tabBar.waitForExistence(timeout: 5)
XCTAssertTrue(reachedMain, "Should reach main tabs after first login to establish completed-onboarding state")
// Step 2: Terminate the app
app.terminate()
// Step 3: Relaunch WITHOUT --reset-state so the onboarding-completed flag is preserved.
// This simulates a real app restart where the user should NOT see onboarding again.
app.launchArguments = [
"--ui-testing",
"--disable-animations"
// NOTE: intentionally omitting --reset-state
]
app.launch()
app.otherElements[UITestID.Root.ready].waitForExistenceOrFail(timeout: defaultTimeout)
// Step 4: The app should NOT show the onboarding welcome screen.
// It should land on the login screen (token expired/missing) or main tabs
// (if the auth token persisted). Either outcome is valid what matters is
// that the onboarding root is NOT shown.
let onboardingWelcomeTitle = app.descendants(matching: .any)
.matching(identifier: UITestID.Onboarding.welcomeTitle).firstMatch
let startFreshButton = app.descendants(matching: .any)
.matching(identifier: UITestID.Onboarding.startFreshButton).firstMatch
// Give the app a moment to settle on its landing screen
sleep(2)
let isShowingOnboarding = onboardingWelcomeTitle.exists || startFreshButton.exists
XCTAssertFalse(
isShowingOnboarding,
"App should NOT show the onboarding welcome screen after onboarding was completed on a previous launch"
)
// Additionally verify the app landed on a valid post-onboarding screen
let loginField = app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
let isOnLogin = loginField.waitForExistence(timeout: defaultTimeout)
let isOnMain = mainTabs.exists || tabBar.exists
XCTAssertTrue(
isOnLogin || isOnMain,
"After relaunch without reset, app should show login or main tabs — not onboarding"
)
}
}