Fix root causes uncovered across repeated parallel runs: - Admin seed password "test1234" failed backend complexity (needs uppercase). Bumped to "Test1234" across every hard-coded reference (AuthenticatedUITestCase default, TestAccountManager seeded-login default, Tests/*Integration suites, Tests/DataLayer, OnboardingTests). - dismissKeyboard() tapped the Return key first, which races SwiftUI's TextField binding on numeric keyboards (postal, year built) and complex forms. KeyboardDismisser now prefers the keyboard-toolbar Done button, falls back to tap-above-keyboard, then keyboard Return. BaseUITestCase.clearAndEnterText uses the same helper. - Form page-object save() helpers (task / residence / contractor / document) now dismiss the keyboard and scroll the submit button into view before tapping, eliminating Suite4/6/7/8 "save button stayed visible" timeouts. - Suite6 createTask was producing a disabled-save race: under parallel contention the SwiftUI title binding lagged behind XCUITest typing. Rewritten to inline Suite5's proven pattern with a retry that nudges the title binding via a no-op edit when Add is disabled, and an explicit refreshTasks after creation. - Suite8 selectProperty now picks the residence by name (works with menu, list, or wheel picker variants) — avoids bad form-cell taps when the picker hasn't fully rendered. - run_ui_tests.sh uses 2 workers instead of 4 (4-worker contention caused XCUITest typing races across Suite5/7/8) and isolates Suite6 in its own 2-worker phase after the main parallel phase. - Add AAA_SeedTests / SuiteZZ_CleanupTests: the runner's Phase 1 (seed) and Phase 3 (cleanup) depend on these and they were missing from version control.
274 lines
13 KiB
Swift
274 lines
13 KiB
Swift
import XCTest
|
|
|
|
final class OnboardingTests: BaseUITestCase {
|
|
override var relaunchBetweenTests: Bool { true }
|
|
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)
|
|
nameField.focusAndType("My Test Home", app: app)
|
|
|
|
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() throws {
|
|
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.focusAndType(creds.username, app: app)
|
|
|
|
onbEmailField.waitForExistenceOrFail(timeout: defaultTimeout)
|
|
onbEmailField.focusAndType(creds.email, app: app)
|
|
|
|
onbPasswordField.waitForExistenceOrFail(timeout: defaultTimeout)
|
|
onbPasswordField.focusAndType(creds.password, app: app)
|
|
|
|
onbConfirmPasswordField.waitForExistenceOrFail(timeout: defaultTimeout)
|
|
onbConfirmPasswordField.focusAndType(creds.password, app: app)
|
|
|
|
// 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)
|
|
// If the create account button was disabled (password fields didn't fill),
|
|
// we won't reach verification. Check before asserting.
|
|
let verificationLoaded = verificationScreen.codeField.waitForExistence(timeout: loginTimeout)
|
|
guard verificationLoaded else {
|
|
// Check if the create account button is still visible (form submission failed)
|
|
if createAccountButton.exists {
|
|
throw XCTSkip("Create account form submission did not proceed to verification — password fields may not have received input")
|
|
}
|
|
XCTFail("Expected verification screen to load")
|
|
return
|
|
}
|
|
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: loginTimeout)
|
|
|| 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 = LoginScreenObject(app: app)
|
|
// The login sheet may take time to appear after onboarding transition
|
|
let loginFieldAppeared = app.textFields[UITestID.Auth.usernameField].waitForExistence(timeout: loginTimeout)
|
|
guard loginFieldAppeared else {
|
|
// If already on main tabs (persisted session), skip login
|
|
if app.tabBars.firstMatch.exists { /* continue to Step 2 */ }
|
|
else { XCTFail("Login screen did not appear after tapping Already Have Account"); return }
|
|
return
|
|
}
|
|
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: loginTimeout)
|
|
|| 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
|
|
|
|
// Wait for the app to settle on its landing screen
|
|
let loginField = app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
|
|
_ = loginField.waitForExistence(timeout: defaultTimeout)
|
|
|| mainTabs.waitForExistence(timeout: 3)
|
|
|| tabBar.waitForExistence(timeout: 3)
|
|
|
|
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 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"
|
|
)
|
|
}
|
|
}
|