- 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>
258 lines
12 KiB
Swift
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"
|
|
)
|
|
}
|
|
}
|