Merge branch 'develop' of github.com:akatreyt/MyCribKMM into develop
# Conflicts: # iosApp/CaseraUITests/SimpleLoginTest.swift # iosApp/CaseraUITests/Suite0_OnboardingTests.swift # iosApp/CaseraUITests/Suite10_ComprehensiveE2ETests.swift # iosApp/CaseraUITests/Suite1_RegistrationTests.swift # iosApp/CaseraUITests/Suite2_AuthenticationTests.swift # iosApp/CaseraUITests/Suite3_ResidenceTests.swift # iosApp/CaseraUITests/Suite4_ComprehensiveResidenceTests.swift # iosApp/CaseraUITests/Suite5_TaskTests.swift # iosApp/CaseraUITests/Suite6_ComprehensiveTaskTests.swift # iosApp/CaseraUITests/Suite7_ContractorTests.swift # iosApp/CaseraUITests/Suite8_DocumentWarrantyTests.swift # iosApp/CaseraUITests/Suite9_IntegrationE2ETests.swift # iosApp/CaseraUITests/UITestHelpers.swift # iosApp/iosApp/RootView.swift # iosApp/iosApp/iOSApp.swift
This commit is contained in:
@@ -9,7 +9,7 @@ package com.example.casera.network
|
||||
*/
|
||||
object ApiConfig {
|
||||
// ⚠️ CHANGE THIS TO TOGGLE ENVIRONMENT ⚠️
|
||||
val CURRENT_ENV = Environment.DEV
|
||||
val CURRENT_ENV = Environment.LOCAL
|
||||
|
||||
enum class Environment {
|
||||
LOCAL,
|
||||
|
||||
@@ -14,4 +14,4 @@ android.useAndroidX=true
|
||||
|
||||
kotlin.native.binary.objcDisposeOnMain=false
|
||||
|
||||
org.gradle.java.home=/Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home
|
||||
org.gradle.java.home=/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home
|
||||
|
||||
164
iosApp/CaseraUITests/Docs/Failing_Suites_0_3_Rebuild_Plan.md
Normal file
164
iosApp/CaseraUITests/Docs/Failing_Suites_0_3_Rebuild_Plan.md
Normal file
@@ -0,0 +1,164 @@
|
||||
# Failing Suites 0-3: Coverage + Rebuild Plan
|
||||
|
||||
## Baseline (from observed runs)
|
||||
- `Suite0_OnboardingTests`: 1 test, 1 failure
|
||||
- `Suite1_RegistrationTests`: 11 tests, 5 failures
|
||||
- `Suite2_AuthenticationTests`: 6 tests, 2 failures
|
||||
- `Suite3_ResidenceTests`: 6 tests, 6 failures
|
||||
|
||||
Primary failure logs used:
|
||||
- `/tmp/ui_suite0.log`
|
||||
- `/tmp/ui_suites_1_3.log`
|
||||
|
||||
---
|
||||
|
||||
## Suite0
|
||||
|
||||
### Failing test
|
||||
- `Suite0_OnboardingTests.test_onboarding`
|
||||
|
||||
### What it is testing
|
||||
- End-to-end onboarding progression from welcome/login entry into account creation and onward.
|
||||
- UI interaction stability during onboarding form entry.
|
||||
|
||||
### Observed failure point
|
||||
- Assertion failure: `Email field must become focused for typing`.
|
||||
|
||||
### Rebuild in new arch
|
||||
Create a new test case focused on deterministic onboarding field interaction:
|
||||
- `Onboarding_EmailRegistration_FocusAndInputFlow`
|
||||
|
||||
Coverage to preserve:
|
||||
- Email field reliably focusable and typeable.
|
||||
- Continue action only enabled after valid required inputs.
|
||||
- Onboarding progresses to next state after valid submission.
|
||||
|
||||
Required infra:
|
||||
- `OnboardingScreen` page object with `tapEmailField()`, `typeEmail()`, `assertEmailFieldFocused()`.
|
||||
- Keyboard/overlay helper centralized (not inline in tests).
|
||||
|
||||
---
|
||||
|
||||
## Suite1
|
||||
Detailed plan already captured in:
|
||||
- `/Users/treyt/Desktop/code/MyCribKMM/iosApp/CaseraUITests/Docs/Suite1_Failing_Test_Rebuild_Plan.md`
|
||||
|
||||
### Failing tests
|
||||
- `test07_successfulRegistrationAndVerification`
|
||||
- `test09_registrationWithInvalidVerificationCode`
|
||||
- `test10_verificationCodeFieldValidation`
|
||||
- `test11_appRelaunchWithUnverifiedUser`
|
||||
- `test12_logoutFromVerificationScreen`
|
||||
|
||||
### Rebuild targets
|
||||
- `Registration_HappyPath_CompletesVerification_ThenCanLogout`
|
||||
- `Registration_InvalidVerifyCode_ShowsError_StaysUnverified`
|
||||
- `Registration_IncompleteVerifyCode_DoesNotVerify`
|
||||
- `Registration_UnverifiedUser_RelaunchStillBlockedFromMain`
|
||||
- `Registration_VerificationScreenLogout_ReturnsToLogin`
|
||||
|
||||
---
|
||||
|
||||
## Suite2
|
||||
|
||||
### Failing tests
|
||||
- `Suite2_AuthenticationTests.test02_loginWithValidCredentials`
|
||||
- `Suite2_AuthenticationTests.test06_logout`
|
||||
|
||||
### What they are testing
|
||||
|
||||
#### `test02_loginWithValidCredentials`
|
||||
- Valid login path transitions from login screen to main app.
|
||||
- Authenticated state exposes main navigation (tab bar/app root).
|
||||
|
||||
#### `test06_logout`
|
||||
- Logged-in user can logout.
|
||||
- Session is cleared and app returns to login state.
|
||||
|
||||
### Observed failure points
|
||||
- `test02`: `Should navigate to main app after successful login`
|
||||
- `test06`: `Should be logged in` (precondition for logout flow failed)
|
||||
|
||||
### Rebuild in new arch
|
||||
Create explicit state-driven auth tests:
|
||||
- `Auth_ValidLogin_TransitionsToMainApp`
|
||||
- `Auth_Logout_FromMainApp_ReturnsToLogin`
|
||||
|
||||
Coverage to preserve:
|
||||
- Login success sets authenticated UI state.
|
||||
- Logout always clears authenticated state.
|
||||
- No false-positive “logged in” assumptions.
|
||||
|
||||
Required infra:
|
||||
- `LoginScreen`, `MainTabScreen`, `ProfileScreen` page objects.
|
||||
- `AuthAssertions.assertAtLoginRoot()`, `assertAtMainRoot()`.
|
||||
- Test user fixture policy for valid credentials.
|
||||
|
||||
---
|
||||
|
||||
## Suite3
|
||||
|
||||
### Failing tests
|
||||
- `Suite3_ResidenceTests.test01_viewResidencesList`
|
||||
- `Suite3_ResidenceTests.test02_navigateToAddResidence`
|
||||
- `Suite3_ResidenceTests.test03_navigationBetweenTabs`
|
||||
- `Suite3_ResidenceTests.test04_cancelResidenceCreation`
|
||||
- `Suite3_ResidenceTests.test05_createResidenceWithMinimalData`
|
||||
- `Suite3_ResidenceTests.test06_viewResidenceDetails`
|
||||
|
||||
### What they are testing
|
||||
- Residence tab/list visibility.
|
||||
- Navigation to add-residence form.
|
||||
- Cross-tab navigation sanity.
|
||||
- Canceling residence creation.
|
||||
- Creating residence with minimal fields.
|
||||
- Opening residence details.
|
||||
|
||||
### Observed failure pattern
|
||||
All 6 fail at the same gateway:
|
||||
- No `Residences` tab bar button match found.
|
||||
- This indicates tests are not reaching authenticated main-app state before residence assertions.
|
||||
|
||||
### Rebuild in new arch
|
||||
Split auth precondition from residence behavior:
|
||||
- `Residence_Precondition_AuthenticatedAndAtResidencesTab`
|
||||
- `Residence_OpenCreateForm`
|
||||
- `Residence_CancelCreate_ReturnsToList`
|
||||
- `Residence_CreateMinimal_ShowsInList`
|
||||
- `Residence_OpenDetails_FromList`
|
||||
- `Residence_TabNavigation_MainSections`
|
||||
|
||||
Coverage to preserve:
|
||||
- Residence flows validated only after explicit `main app ready` assertion.
|
||||
- Failures clearly classify as auth-gate vs residence-feature regression.
|
||||
|
||||
Required infra:
|
||||
- `MainTabScreen.goToResidences()` with ID-first selectors.
|
||||
- `ResidenceListScreen`, `ResidenceFormScreen`, `ResidenceDetailScreen` page objects.
|
||||
- Shared precondition helper: `ensureAuthenticatedMainApp()`.
|
||||
|
||||
---
|
||||
|
||||
## Blueprint-aligned migration notes
|
||||
- Keep old-to-new mapping explicit in PR description.
|
||||
- Replace brittle text-based selectors with accessibility IDs first.
|
||||
- Use one state assertion per transition boundary:
|
||||
- `login -> verification -> main app -> login`.
|
||||
- Move keyboard/strong-password overlay handling into one helper.
|
||||
- Do not mark legacy tests removed until replacement coverage is green.
|
||||
|
||||
## Proposed replacement matrix
|
||||
- `Suite0.test_onboarding` -> `Onboarding_EmailRegistration_FocusAndInputFlow`
|
||||
- `Suite1.test07` -> `Registration_HappyPath_CompletesVerification_ThenCanLogout`
|
||||
- `Suite1.test09` -> `Registration_InvalidVerifyCode_ShowsError_StaysUnverified`
|
||||
- `Suite1.test10` -> `Registration_IncompleteVerifyCode_DoesNotVerify`
|
||||
- `Suite1.test11` -> `Registration_UnverifiedUser_RelaunchStillBlockedFromMain`
|
||||
- `Suite1.test12` -> `Registration_VerificationScreenLogout_ReturnsToLogin`
|
||||
- `Suite2.test02` -> `Auth_ValidLogin_TransitionsToMainApp`
|
||||
- `Suite2.test06` -> `Auth_Logout_FromMainApp_ReturnsToLogin`
|
||||
- `Suite3.test01` -> `Residence_Precondition_AuthenticatedAndAtResidencesTab`
|
||||
- `Suite3.test02` -> `Residence_OpenCreateForm`
|
||||
- `Suite3.test03` -> `Residence_TabNavigation_MainSections`
|
||||
- `Suite3.test04` -> `Residence_CancelCreate_ReturnsToList`
|
||||
- `Suite3.test05` -> `Residence_CreateMinimal_ShowsInList`
|
||||
- `Suite3.test06` -> `Residence_OpenDetails_FromList`
|
||||
174
iosApp/CaseraUITests/Docs/Suite1_Failing_Test_Rebuild_Plan.md
Normal file
174
iosApp/CaseraUITests/Docs/Suite1_Failing_Test_Rebuild_Plan.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# Suite1 Registration Failing Tests: Coverage + Rebuild Plan
|
||||
|
||||
## Scope
|
||||
This document captures what the currently failing registration-flow tests are trying to validate and how to recreate that coverage using the new UI test architecture.
|
||||
|
||||
Source tests:
|
||||
- `Suite1_RegistrationTests.test07_successfulRegistrationAndVerification`
|
||||
- `Suite1_RegistrationTests.test09_registrationWithInvalidVerificationCode`
|
||||
- `Suite1_RegistrationTests.test10_verificationCodeFieldValidation`
|
||||
- `Suite1_RegistrationTests.test11_appRelaunchWithUnverifiedUser`
|
||||
- `Suite1_RegistrationTests.test12_logoutFromVerificationScreen`
|
||||
|
||||
## Current Failure Context (Observed)
|
||||
- Registration submit does not transition to a verification screen in automation runs.
|
||||
- UI-level registration error shown during failures: `Password must be at least 8 characters`.
|
||||
- Because registration transition fails, downstream verification assertions fail.
|
||||
|
||||
## What Each Failing Test Is Actually Testing
|
||||
|
||||
### 1) `test07_successfulRegistrationAndVerification`
|
||||
Behavior intent:
|
||||
- User can register with valid credentials.
|
||||
- App transitions to verification state.
|
||||
- Entering valid verification code completes verification.
|
||||
- User lands in main app (tab bar available).
|
||||
- Logout returns user to login.
|
||||
|
||||
Core business coverage:
|
||||
- Happy-path onboarding/auth state progression.
|
||||
- Verified user session gains app access.
|
||||
- Logout clears authenticated session.
|
||||
|
||||
### 2) `test09_registrationWithInvalidVerificationCode`
|
||||
Behavior intent:
|
||||
- Registration reaches verification state.
|
||||
- Entering wrong code shows verification error.
|
||||
- User remains blocked from main app.
|
||||
|
||||
Core business coverage:
|
||||
- Backend validation for invalid verification code.
|
||||
- No false positive promotion to verified state.
|
||||
|
||||
### 3) `test10_verificationCodeFieldValidation`
|
||||
Behavior intent:
|
||||
- Verification screen enforces code format/length.
|
||||
- Incomplete code does not complete verification.
|
||||
- User remains on verification state.
|
||||
|
||||
Core business coverage:
|
||||
- Client-side verification input guardrails.
|
||||
- No bypass with partial code.
|
||||
|
||||
### 4) `test11_appRelaunchWithUnverifiedUser`
|
||||
Behavior intent:
|
||||
- User reaches unverified verification state.
|
||||
- App terminate/relaunch preserves unverified gating.
|
||||
- Relaunch must not allow direct main-app access.
|
||||
|
||||
Core business coverage:
|
||||
- Session restore + auth gate correctness for unverified users.
|
||||
|
||||
### 5) `test12_logoutFromVerificationScreen`
|
||||
Behavior intent:
|
||||
- Unverified user can explicitly logout from verification screen.
|
||||
- Verification UI dismisses.
|
||||
- App returns to interactive login screen.
|
||||
|
||||
Core business coverage:
|
||||
- Logout works from gated verification state.
|
||||
- Session cleanup from pre-verified auth state.
|
||||
|
||||
## Rebuild These in New Architecture
|
||||
|
||||
## Shared Test Architecture Requirements
|
||||
Create/ensure these reusable pieces:
|
||||
- `AuthFlowHarness` (launch + auth preconditions + cleanup)
|
||||
- `RegistrationScreen` page object
|
||||
- `VerificationScreen` page object
|
||||
- `MainTabScreen` page object
|
||||
- `SessionStateAsserts` helpers for `login`, `verification`, `mainApp`
|
||||
- `TestUserFactory` with deterministic unique users
|
||||
|
||||
Use stable selectors first:
|
||||
- Accessibility IDs over title text.
|
||||
- Support both auth/onboarding verification IDs only if product can route to either screen.
|
||||
|
||||
## Suggested New-Arch Test Cases (One-to-One Replacement)
|
||||
|
||||
### A. `Registration_HappyPath_CompletesVerification_ThenCanLogout`
|
||||
Covers legacy test07.
|
||||
|
||||
Given:
|
||||
- Fresh launch, logged out.
|
||||
|
||||
When:
|
||||
- Register with valid user.
|
||||
- Verify with valid code.
|
||||
- Logout from profile/main app.
|
||||
|
||||
Then:
|
||||
- Verification gate appears after register.
|
||||
- Main app appears only after successful verify.
|
||||
- Logout returns to login root.
|
||||
|
||||
### B. `Registration_InvalidVerifyCode_ShowsError_StaysUnverified`
|
||||
Covers legacy test09.
|
||||
|
||||
Given:
|
||||
- User registered and on verification screen.
|
||||
|
||||
When:
|
||||
- Submit invalid verification code.
|
||||
|
||||
Then:
|
||||
- Error banner/message visible.
|
||||
- Verification screen remains active.
|
||||
- Main app root not accessible.
|
||||
|
||||
### C. `Registration_IncompleteVerifyCode_DoesNotVerify`
|
||||
Covers legacy test10.
|
||||
|
||||
Given:
|
||||
- User on verification screen.
|
||||
|
||||
When:
|
||||
- Enter fewer than required digits.
|
||||
- Attempt verify (or assert button disabled).
|
||||
|
||||
Then:
|
||||
- Verification completion does not occur.
|
||||
- User remains blocked from main app.
|
||||
|
||||
### D. `Registration_UnverifiedUser_RelaunchStillBlockedFromMain`
|
||||
Covers legacy test11.
|
||||
|
||||
Given:
|
||||
- User registered but not verified.
|
||||
|
||||
When:
|
||||
- Terminate and relaunch app.
|
||||
|
||||
Then:
|
||||
- User is on verification gate (or login if session invalidated).
|
||||
- User is never placed directly in main app state.
|
||||
|
||||
### E. `Registration_VerificationScreenLogout_ReturnsToLogin`
|
||||
Covers legacy test12.
|
||||
|
||||
Given:
|
||||
- User at verification gate.
|
||||
|
||||
When:
|
||||
- Tap logout on verification screen.
|
||||
|
||||
Then:
|
||||
- Verification state exits.
|
||||
- Login root becomes active and interactive.
|
||||
|
||||
## Data + Environment Strategy for Rebuild
|
||||
- Use API mode/environment that is stable for registration + verification in CI and local runs.
|
||||
- Seed/fixture verification code contract must be explicit (example: fixed debug code).
|
||||
- Generate unique username/email per test to avoid collisions.
|
||||
- If keyboard autofill overlays are flaky, centralize handling in input helper (not per-test).
|
||||
|
||||
## Migration Notes
|
||||
- Keep legacy tests disabled/removed only after each replacement test is green.
|
||||
- Track replacement mapping in PR description:
|
||||
- `old test -> new test`
|
||||
- Preserve negative assertions ("must NOT access main app before verify").
|
||||
|
||||
## Open Risks To Resolve During Rebuild
|
||||
- Registration password entry flakiness from iOS strong-password UI overlays.
|
||||
- Potential mismatch between onboarding verification screen IDs and auth verification screen IDs.
|
||||
- Environment-dependent backend behavior (local/dev) affecting registration transition.
|
||||
124
iosApp/CaseraUITests/Framework/BaseUITestCase.swift
Normal file
124
iosApp/CaseraUITests/Framework/BaseUITestCase.swift
Normal file
@@ -0,0 +1,124 @@
|
||||
import XCTest
|
||||
|
||||
class BaseUITestCase: XCTestCase {
|
||||
let app = XCUIApplication()
|
||||
|
||||
let shortTimeout: TimeInterval = 5
|
||||
let defaultTimeout: TimeInterval = 15
|
||||
let longTimeout: TimeInterval = 30
|
||||
|
||||
var includeResetStateLaunchArgument: Bool { true }
|
||||
var additionalLaunchArguments: [String] { [] }
|
||||
|
||||
override func setUpWithError() throws {
|
||||
continueAfterFailure = false
|
||||
XCUIDevice.shared.orientation = .portrait
|
||||
|
||||
var launchArguments = [
|
||||
"--ui-testing",
|
||||
"--disable-animations"
|
||||
]
|
||||
if includeResetStateLaunchArgument {
|
||||
launchArguments.append("--reset-state")
|
||||
}
|
||||
launchArguments.append(contentsOf: additionalLaunchArguments)
|
||||
app.launchArguments = launchArguments
|
||||
|
||||
app.launch()
|
||||
app.otherElements["ui.app.ready"].waitForExistenceOrFail(timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
if let run = testRun, !run.hasSucceeded {
|
||||
let attachment = XCTAttachment(screenshot: app.screenshot())
|
||||
attachment.name = "Failure-\(name)"
|
||||
attachment.lifetime = .keepAlways
|
||||
add(attachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension XCUIElement {
|
||||
@discardableResult
|
||||
func waitForExistenceOrFail(
|
||||
timeout: TimeInterval,
|
||||
message: String? = nil,
|
||||
file: StaticString = #filePath,
|
||||
line: UInt = #line
|
||||
) -> XCUIElement {
|
||||
if !waitForExistence(timeout: timeout) {
|
||||
XCTFail(message ?? "Expected element to exist: \(self)", file: file, line: line)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func waitUntilHittable(
|
||||
timeout: TimeInterval,
|
||||
message: String? = nil,
|
||||
file: StaticString = #filePath,
|
||||
line: UInt = #line
|
||||
) -> XCUIElement {
|
||||
let predicate = NSPredicate(format: "exists == true AND hittable == true")
|
||||
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: self)
|
||||
let result = XCTWaiter().wait(for: [expectation], timeout: timeout)
|
||||
|
||||
if result != .completed {
|
||||
XCTFail(message ?? "Expected element to become hittable: \(self)", file: file, line: line)
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func waitForNonExistence(
|
||||
timeout: TimeInterval,
|
||||
message: String? = nil,
|
||||
file: StaticString = #filePath,
|
||||
line: UInt = #line
|
||||
) -> Bool {
|
||||
let predicate = NSPredicate(format: "exists == false")
|
||||
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: self)
|
||||
let result = XCTWaiter().wait(for: [expectation], timeout: timeout)
|
||||
|
||||
if result != .completed {
|
||||
XCTFail(message ?? "Expected element to disappear: \(self)", file: file, line: line)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func scrollIntoView(
|
||||
in scrollView: XCUIElement,
|
||||
maxSwipes: Int = 8,
|
||||
file: StaticString = #filePath,
|
||||
line: UInt = #line
|
||||
) {
|
||||
if isHittable { return }
|
||||
|
||||
for _ in 0..<maxSwipes {
|
||||
scrollView.swipeUp()
|
||||
if isHittable { return }
|
||||
}
|
||||
|
||||
for _ in 0..<maxSwipes {
|
||||
scrollView.swipeDown()
|
||||
if isHittable { return }
|
||||
}
|
||||
|
||||
XCTFail("Failed to scroll element into view: \(self)", file: file, line: line)
|
||||
}
|
||||
|
||||
func forceTap(file: StaticString = #filePath, line: UInt = #line) {
|
||||
if isHittable {
|
||||
tap()
|
||||
return
|
||||
}
|
||||
if exists {
|
||||
coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||
return
|
||||
}
|
||||
XCTFail("Expected element to exist before forceTap: \(self)", file: file, line: line)
|
||||
}
|
||||
}
|
||||
183
iosApp/CaseraUITests/Framework/RebuildSupport.swift
Normal file
183
iosApp/CaseraUITests/Framework/RebuildSupport.swift
Normal file
@@ -0,0 +1,183 @@
|
||||
import XCTest
|
||||
|
||||
struct RebuildTestUser {
|
||||
let username: String
|
||||
let email: String
|
||||
let password: String
|
||||
}
|
||||
|
||||
enum RebuildTestUserFactory {
|
||||
static func unique(prefix: String = "uit") -> RebuildTestUser {
|
||||
let stamp = Int(Date().timeIntervalSince1970)
|
||||
return RebuildTestUser(
|
||||
username: "\(prefix)_user_\(stamp)",
|
||||
email: "\(prefix)_\(stamp)@example.com",
|
||||
password: "Pass1234"
|
||||
)
|
||||
}
|
||||
|
||||
static var seeded: RebuildTestUser {
|
||||
RebuildTestUser(username: "testuser", email: "test@example.com", password: "TestPass123!")
|
||||
}
|
||||
}
|
||||
|
||||
struct VerificationScreen {
|
||||
let app: XCUIApplication
|
||||
|
||||
private var authCodeField: XCUIElement { app.textFields[AccessibilityIdentifiers.Authentication.verificationCodeField] }
|
||||
private var onboardingCodeField: XCUIElement { app.textFields[AccessibilityIdentifiers.Onboarding.verificationCodeField] }
|
||||
private var authVerifyButton: XCUIElement { app.buttons[AccessibilityIdentifiers.Authentication.verifyButton] }
|
||||
private var onboardingVerifyButton: XCUIElement { app.buttons[AccessibilityIdentifiers.Onboarding.verifyButton] }
|
||||
|
||||
var codeField: XCUIElement {
|
||||
if authCodeField.exists { return authCodeField }
|
||||
return onboardingCodeField
|
||||
}
|
||||
|
||||
var verifyButton: XCUIElement {
|
||||
if authVerifyButton.exists { return authVerifyButton }
|
||||
if onboardingVerifyButton.exists { return onboardingVerifyButton }
|
||||
return app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Verify'")).firstMatch
|
||||
}
|
||||
|
||||
func waitForLoad(timeout: TimeInterval = 15, file: StaticString = #filePath, line: UInt = #line) {
|
||||
let loaded = authCodeField.waitForExistence(timeout: timeout)
|
||||
|| onboardingCodeField.waitForExistence(timeout: timeout)
|
||||
|| authVerifyButton.waitForExistence(timeout: timeout)
|
||||
|| onboardingVerifyButton.waitForExistence(timeout: timeout)
|
||||
XCTAssertTrue(loaded, "Expected verification screen to load", file: file, line: line)
|
||||
}
|
||||
|
||||
func enterCode(_ code: String) {
|
||||
codeField.waitForExistenceOrFail(timeout: 10)
|
||||
codeField.forceTap()
|
||||
codeField.typeText(code)
|
||||
}
|
||||
|
||||
func submitCode() {
|
||||
verifyButton.waitForExistenceOrFail(timeout: 10)
|
||||
verifyButton.forceTap()
|
||||
}
|
||||
|
||||
func tapLogoutIfAvailable() {
|
||||
let logout = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Logout' OR label CONTAINS[c] 'Log Out'")).firstMatch
|
||||
if logout.waitForExistence(timeout: 3) {
|
||||
logout.forceTap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MainTabScreen {
|
||||
let app: XCUIApplication
|
||||
|
||||
var tabBar: XCUIElement { app.tabBars.firstMatch }
|
||||
var mainRoot: XCUIElement { app.otherElements[UITestID.Root.mainTabs] }
|
||||
|
||||
var residencesTab: XCUIElement {
|
||||
let byID = app.buttons[AccessibilityIdentifiers.Navigation.residencesTab]
|
||||
if byID.exists { return byID }
|
||||
return app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
|
||||
}
|
||||
|
||||
var profileTab: XCUIElement {
|
||||
let byID = app.buttons[AccessibilityIdentifiers.Navigation.profileTab]
|
||||
if byID.exists { return byID }
|
||||
return app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Profile'")).firstMatch
|
||||
}
|
||||
|
||||
func waitForLoad(timeout: TimeInterval = 15) {
|
||||
let loaded = mainRoot.waitForExistence(timeout: timeout)
|
||||
|| tabBar.waitForExistence(timeout: timeout)
|
||||
XCTAssertTrue(loaded, "Expected main app root to appear")
|
||||
}
|
||||
|
||||
func goToResidences() {
|
||||
residencesTab.waitForExistenceOrFail(timeout: 10)
|
||||
residencesTab.forceTap()
|
||||
}
|
||||
|
||||
func goToProfile() {
|
||||
profileTab.waitForExistenceOrFail(timeout: 10)
|
||||
profileTab.forceTap()
|
||||
}
|
||||
}
|
||||
|
||||
struct ResidenceListScreen {
|
||||
let app: XCUIApplication
|
||||
|
||||
var addButton: XCUIElement {
|
||||
let byID = app.buttons[AccessibilityIdentifiers.Residence.addButton]
|
||||
if byID.exists { return byID }
|
||||
return app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Add'")).firstMatch
|
||||
}
|
||||
|
||||
var list: XCUIElement { app.otherElements[AccessibilityIdentifiers.Residence.residencesList] }
|
||||
var emptyState: XCUIElement { app.otherElements[AccessibilityIdentifiers.Residence.emptyStateView] }
|
||||
var residenceCard: XCUIElement { app.otherElements.matching(identifier: AccessibilityIdentifiers.Residence.residenceCard).firstMatch }
|
||||
|
||||
func waitForLoad(timeout: TimeInterval = 15) {
|
||||
let deadline = Date().addingTimeInterval(timeout)
|
||||
var loaded = false
|
||||
repeat {
|
||||
loaded = list.exists
|
||||
|| emptyState.exists
|
||||
|| residenceCard.exists
|
||||
|| addButton.exists
|
||||
|| app.staticTexts["Residences"].exists
|
||||
if loaded { break }
|
||||
RunLoop.current.run(until: Date().addingTimeInterval(0.2))
|
||||
} while Date() < deadline
|
||||
|
||||
XCTAssertTrue(loaded, "Expected residences list screen to load")
|
||||
}
|
||||
|
||||
func openCreateResidence() {
|
||||
addButton.waitForExistenceOrFail(timeout: 10)
|
||||
addButton.forceTap()
|
||||
}
|
||||
}
|
||||
|
||||
struct ResidenceFormScreen {
|
||||
let app: XCUIApplication
|
||||
|
||||
var nameField: XCUIElement { app.textFields[AccessibilityIdentifiers.Residence.nameField] }
|
||||
var saveButton: XCUIElement { app.buttons[AccessibilityIdentifiers.Residence.saveButton] }
|
||||
var cancelButton: XCUIElement { app.buttons[AccessibilityIdentifiers.Residence.formCancelButton] }
|
||||
|
||||
func waitForLoad(timeout: TimeInterval = 15) {
|
||||
XCTAssertTrue(nameField.waitForExistence(timeout: timeout), "Expected residence form")
|
||||
}
|
||||
|
||||
func enterName(_ value: String) {
|
||||
nameField.waitForExistenceOrFail(timeout: 10)
|
||||
nameField.forceTap()
|
||||
nameField.typeText(value)
|
||||
}
|
||||
|
||||
func save() { saveButton.waitForExistenceOrFail(timeout: 10); saveButton.forceTap() }
|
||||
func cancel() { cancelButton.waitForExistenceOrFail(timeout: 10); cancelButton.forceTap() }
|
||||
}
|
||||
|
||||
enum RebuildSessionAssertions {
|
||||
static func assertOnLogin(_ app: XCUIApplication, timeout: TimeInterval = 15, file: StaticString = #filePath, line: UInt = #line) {
|
||||
let login = LoginScreen(app: app)
|
||||
login.waitForLoad(timeout: timeout)
|
||||
XCTAssertTrue(app.textFields[UITestID.Auth.usernameField].exists, "Expected login state", file: file, line: line)
|
||||
}
|
||||
|
||||
static func assertOnMainApp(_ app: XCUIApplication, timeout: TimeInterval = 15, file: StaticString = #filePath, line: UInt = #line) {
|
||||
let main = MainTabScreen(app: app)
|
||||
main.waitForLoad(timeout: timeout)
|
||||
XCTAssertTrue(
|
||||
app.otherElements[UITestID.Root.mainTabs].exists || main.tabBar.exists,
|
||||
"Expected main app state",
|
||||
file: file,
|
||||
line: line
|
||||
)
|
||||
}
|
||||
|
||||
static func assertOnVerification(_ app: XCUIApplication, timeout: TimeInterval = 15, file: StaticString = #filePath, line: UInt = #line) {
|
||||
let verify = VerificationScreen(app: app)
|
||||
verify.waitForLoad(timeout: timeout, file: file, line: line)
|
||||
}
|
||||
}
|
||||
277
iosApp/CaseraUITests/Framework/ScreenObjects.swift
Normal file
277
iosApp/CaseraUITests/Framework/ScreenObjects.swift
Normal file
@@ -0,0 +1,277 @@
|
||||
import XCTest
|
||||
|
||||
struct UITestID {
|
||||
struct Root {
|
||||
static let ready = "ui.app.ready"
|
||||
static let onboarding = "ui.root.onboarding"
|
||||
static let login = "ui.root.login"
|
||||
static let mainTabs = "ui.root.mainTabs"
|
||||
}
|
||||
|
||||
struct Onboarding {
|
||||
static let welcomeTitle = "Onboarding.WelcomeTitle"
|
||||
static let startFreshButton = "Onboarding.StartFreshButton"
|
||||
static let joinExistingButton = "Onboarding.JoinExistingButton"
|
||||
static let loginButton = "Onboarding.LoginButton"
|
||||
static let valuePropsContainer = "Onboarding.ValuePropsTitle"
|
||||
static let valuePropsNextButton = "Onboarding.ValuePropsNextButton"
|
||||
static let nameResidenceTitle = "Onboarding.NameResidenceTitle"
|
||||
static let residenceNameField = "Onboarding.ResidenceNameField"
|
||||
static let nameResidenceContinueButton = "Onboarding.NameResidenceContinueButton"
|
||||
static let createAccountTitle = "Onboarding.CreateAccountTitle"
|
||||
static let emailSignUpExpandButton = "Onboarding.EmailSignUpExpandButton"
|
||||
static let createAccountButton = "Onboarding.CreateAccountButton"
|
||||
static let backButton = "Onboarding.BackButton"
|
||||
static let skipButton = "Onboarding.SkipButton"
|
||||
static let progressIndicator = "Onboarding.ProgressIndicator"
|
||||
}
|
||||
|
||||
struct Auth {
|
||||
static let usernameField = "Login.UsernameField"
|
||||
static let passwordField = "Login.PasswordField"
|
||||
static let passwordVisibilityToggle = "Login.PasswordVisibilityToggle"
|
||||
static let loginButton = "Login.LoginButton"
|
||||
static let signUpButton = "Login.SignUpButton"
|
||||
static let forgotPasswordButton = "Login.ForgotPasswordButton"
|
||||
|
||||
static let registerUsernameField = "Register.UsernameField"
|
||||
static let registerEmailField = "Register.EmailField"
|
||||
static let registerPasswordField = "Register.PasswordField"
|
||||
static let registerConfirmPasswordField = "Register.ConfirmPasswordField"
|
||||
static let registerButton = "Register.RegisterButton"
|
||||
static let registerCancelButton = "Register.CancelButton"
|
||||
}
|
||||
}
|
||||
|
||||
struct RootScreen {
|
||||
let app: XCUIApplication
|
||||
|
||||
func waitForReady(timeout: TimeInterval = 15) {
|
||||
app.otherElements[UITestID.Root.ready].waitForExistenceOrFail(timeout: timeout)
|
||||
}
|
||||
}
|
||||
|
||||
struct OnboardingWelcomeScreen {
|
||||
let app: XCUIApplication
|
||||
|
||||
private var onboardingRoot: XCUIElement { app.otherElements[UITestID.Root.onboarding] }
|
||||
private var startFreshButton: XCUIElement { app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.startFreshButton).firstMatch }
|
||||
private var joinExistingButton: XCUIElement { app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.joinExistingButton).firstMatch }
|
||||
private var loginButton: XCUIElement { app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.loginButton).firstMatch }
|
||||
private var backButton: XCUIElement { app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.backButton).firstMatch }
|
||||
|
||||
func waitForLoad(timeout: TimeInterval = 15) {
|
||||
onboardingRoot.waitForExistenceOrFail(timeout: timeout)
|
||||
if startFreshButton.waitForExistence(timeout: 2) {
|
||||
return
|
||||
}
|
||||
|
||||
for _ in 0..<4 {
|
||||
if backButton.exists && backButton.isHittable {
|
||||
backButton.tap()
|
||||
}
|
||||
if startFreshButton.waitForExistence(timeout: 2) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !startFreshButton.waitForExistence(timeout: timeout) {
|
||||
XCTFail("Expected onboarding welcome entry point. Debug tree:\n\(app.debugDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
func tapStartFresh() {
|
||||
startFreshButton.waitUntilHittable(timeout: 10).tap()
|
||||
}
|
||||
|
||||
func tapJoinExisting() {
|
||||
joinExistingButton.waitUntilHittable(timeout: 10).tap()
|
||||
}
|
||||
|
||||
func tapAlreadyHaveAccount() {
|
||||
loginButton.waitForExistenceOrFail(timeout: 10)
|
||||
if loginButton.isHittable {
|
||||
loginButton.tap()
|
||||
} else {
|
||||
loginButton.forceTap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct OnboardingValuePropsScreen {
|
||||
let app: XCUIApplication
|
||||
|
||||
private var container: XCUIElement { app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.valuePropsContainer).firstMatch }
|
||||
private var continueButton: XCUIElement { app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.valuePropsNextButton).firstMatch }
|
||||
private var backButton: XCUIElement { app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.backButton).firstMatch }
|
||||
|
||||
func waitForLoad(timeout: TimeInterval = 15) {
|
||||
container.waitForExistenceOrFail(timeout: timeout)
|
||||
}
|
||||
|
||||
func tapContinue() {
|
||||
continueButton.waitUntilHittable(timeout: 10).tap()
|
||||
}
|
||||
|
||||
func tapBack() {
|
||||
backButton.waitForExistenceOrFail(timeout: 10)
|
||||
backButton.forceTap()
|
||||
}
|
||||
}
|
||||
|
||||
struct OnboardingNameResidenceScreen {
|
||||
let app: XCUIApplication
|
||||
|
||||
private var title: XCUIElement { app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.nameResidenceTitle).firstMatch }
|
||||
private var nameField: XCUIElement { app.textFields[UITestID.Onboarding.residenceNameField] }
|
||||
private var continueButton: XCUIElement { app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.nameResidenceContinueButton).firstMatch }
|
||||
private var backButton: XCUIElement { app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.backButton).firstMatch }
|
||||
|
||||
func waitForLoad(timeout: TimeInterval = 15) {
|
||||
title.waitForExistenceOrFail(timeout: timeout)
|
||||
}
|
||||
|
||||
func enterResidenceName(_ value: String) {
|
||||
nameField.waitUntilHittable(timeout: 10).tap()
|
||||
nameField.typeText(value)
|
||||
}
|
||||
|
||||
func tapContinue() {
|
||||
continueButton.waitUntilHittable(timeout: 10).tap()
|
||||
}
|
||||
|
||||
func tapBack() {
|
||||
backButton.waitForExistenceOrFail(timeout: 10)
|
||||
backButton.forceTap()
|
||||
}
|
||||
}
|
||||
|
||||
struct OnboardingCreateAccountScreen {
|
||||
let app: XCUIApplication
|
||||
|
||||
private var title: XCUIElement { app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.createAccountTitle).firstMatch }
|
||||
private var expandEmailButton: XCUIElement { app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.emailSignUpExpandButton).firstMatch }
|
||||
private var createAccountButton: XCUIElement { app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.createAccountButton).firstMatch }
|
||||
|
||||
func waitForLoad(timeout: TimeInterval = 15) {
|
||||
title.waitForExistenceOrFail(timeout: timeout)
|
||||
}
|
||||
|
||||
func expandEmailSignup() {
|
||||
expandEmailButton.waitUntilHittable(timeout: 10).tap()
|
||||
}
|
||||
|
||||
func waitForCreateAccountButton(timeout: TimeInterval = 10) {
|
||||
createAccountButton.waitForExistenceOrFail(timeout: timeout)
|
||||
}
|
||||
}
|
||||
|
||||
struct LoginScreen {
|
||||
let app: XCUIApplication
|
||||
|
||||
private var usernameField: XCUIElement { app.textFields[UITestID.Auth.usernameField] }
|
||||
private var passwordSecureField: XCUIElement { app.secureTextFields[UITestID.Auth.passwordField] }
|
||||
private var passwordVisibleField: XCUIElement { app.textFields[UITestID.Auth.passwordField] }
|
||||
private var loginButton: XCUIElement { app.buttons[UITestID.Auth.loginButton] }
|
||||
private var signUpButton: XCUIElement { app.buttons[UITestID.Auth.signUpButton] }
|
||||
private var forgotPasswordButton: XCUIElement { app.buttons[UITestID.Auth.forgotPasswordButton] }
|
||||
private var visibilityToggle: XCUIElement { app.buttons[UITestID.Auth.passwordVisibilityToggle] }
|
||||
|
||||
func waitForLoad(timeout: TimeInterval = 15) {
|
||||
usernameField.waitForExistenceOrFail(timeout: timeout)
|
||||
loginButton.waitForExistenceOrFail(timeout: timeout)
|
||||
}
|
||||
|
||||
func enterUsername(_ username: String) {
|
||||
usernameField.waitUntilHittable(timeout: 10).tap()
|
||||
usernameField.typeText(username)
|
||||
}
|
||||
|
||||
func enterPassword(_ password: String) {
|
||||
if passwordSecureField.exists {
|
||||
passwordSecureField.tap()
|
||||
passwordSecureField.typeText(password)
|
||||
} else {
|
||||
passwordVisibleField.waitUntilHittable(timeout: 10).tap()
|
||||
passwordVisibleField.typeText(password)
|
||||
}
|
||||
}
|
||||
|
||||
func tapPasswordVisibilityToggle() {
|
||||
visibilityToggle.waitUntilHittable(timeout: 10).tap()
|
||||
}
|
||||
|
||||
func tapSignUp() {
|
||||
signUpButton.waitUntilHittable(timeout: 10).tap()
|
||||
}
|
||||
|
||||
func tapForgotPassword() {
|
||||
forgotPasswordButton.waitUntilHittable(timeout: 10).tap()
|
||||
}
|
||||
|
||||
func assertPasswordFieldVisible() {
|
||||
XCTAssertTrue(passwordVisibleField.waitForExistence(timeout: 5), "Expected visible password text field after toggle")
|
||||
}
|
||||
}
|
||||
|
||||
struct RegisterScreen {
|
||||
let app: XCUIApplication
|
||||
|
||||
private var usernameField: XCUIElement { app.textFields[UITestID.Auth.registerUsernameField] }
|
||||
private var emailField: XCUIElement { app.textFields[UITestID.Auth.registerEmailField] }
|
||||
private var passwordField: XCUIElement { app.secureTextFields[UITestID.Auth.registerPasswordField] }
|
||||
private var confirmPasswordField: XCUIElement { app.secureTextFields[UITestID.Auth.registerConfirmPasswordField] }
|
||||
private var registerButton: XCUIElement { app.buttons[UITestID.Auth.registerButton] }
|
||||
private var cancelButton: XCUIElement { app.buttons[UITestID.Auth.registerCancelButton] }
|
||||
|
||||
func waitForLoad(timeout: TimeInterval = 15) {
|
||||
usernameField.waitForExistenceOrFail(timeout: timeout)
|
||||
registerButton.waitForExistenceOrFail(timeout: timeout)
|
||||
}
|
||||
|
||||
func fill(username: String, email: String, password: String) {
|
||||
func advanceToNextField() {
|
||||
let keys = ["Next", "Return", "return", "Done", "done"]
|
||||
for key in keys {
|
||||
let button = app.keyboards.buttons[key]
|
||||
if button.waitForExistence(timeout: 1) && button.isHittable {
|
||||
button.tap()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
usernameField.waitForExistenceOrFail(timeout: 10)
|
||||
usernameField.forceTap()
|
||||
usernameField.typeText(username)
|
||||
advanceToNextField()
|
||||
|
||||
emailField.waitForExistenceOrFail(timeout: 10)
|
||||
if !emailField.hasKeyboardFocus {
|
||||
emailField.forceTap()
|
||||
if !emailField.hasKeyboardFocus {
|
||||
advanceToNextField()
|
||||
emailField.forceTap()
|
||||
}
|
||||
}
|
||||
emailField.typeText(email)
|
||||
advanceToNextField()
|
||||
|
||||
passwordField.waitForExistenceOrFail(timeout: 10)
|
||||
if !passwordField.hasKeyboardFocus {
|
||||
passwordField.forceTap()
|
||||
}
|
||||
passwordField.typeText(password)
|
||||
advanceToNextField()
|
||||
|
||||
confirmPasswordField.waitForExistenceOrFail(timeout: 10)
|
||||
if !confirmPasswordField.hasKeyboardFocus {
|
||||
confirmPasswordField.forceTap()
|
||||
}
|
||||
confirmPasswordField.typeText(password)
|
||||
}
|
||||
|
||||
func tapCancel() {
|
||||
cancelButton.waitUntilHittable(timeout: 10).tap()
|
||||
}
|
||||
}
|
||||
54
iosApp/CaseraUITests/Framework/TestFlows.swift
Normal file
54
iosApp/CaseraUITests/Framework/TestFlows.swift
Normal file
@@ -0,0 +1,54 @@
|
||||
import XCTest
|
||||
|
||||
enum TestFlows {
|
||||
@discardableResult
|
||||
static func navigateToLoginFromOnboarding(app: XCUIApplication) -> LoginScreen {
|
||||
let welcome = OnboardingWelcomeScreen(app: app)
|
||||
welcome.waitForLoad()
|
||||
welcome.tapAlreadyHaveAccount()
|
||||
|
||||
let login = LoginScreen(app: app)
|
||||
login.waitForLoad()
|
||||
return login
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
static func navigateStartFreshToCreateAccount(
|
||||
app: XCUIApplication,
|
||||
residenceName: String = "UI Test Residence"
|
||||
) -> OnboardingCreateAccountScreen {
|
||||
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.enterResidenceName(residenceName)
|
||||
nameResidence.tapContinue()
|
||||
|
||||
let createAccount = OnboardingCreateAccountScreen(app: app)
|
||||
createAccount.waitForLoad()
|
||||
return createAccount
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
static func openRegisterFromLogin(app: XCUIApplication) -> RegisterScreen {
|
||||
let login: LoginScreen
|
||||
let loginRoot = app.otherElements[UITestID.Root.login]
|
||||
if loginRoot.exists || app.textFields[UITestID.Auth.usernameField].exists {
|
||||
login = LoginScreen(app: app)
|
||||
login.waitForLoad()
|
||||
} else {
|
||||
login = navigateToLoginFromOnboarding(app: app)
|
||||
}
|
||||
login.tapSignUp()
|
||||
|
||||
let register = RegisterScreen(app: app)
|
||||
register.waitForLoad()
|
||||
return register
|
||||
}
|
||||
}
|
||||
62
iosApp/CaseraUITests/SimpleLoginTest.swift
Normal file
62
iosApp/CaseraUITests/SimpleLoginTest.swift
Normal file
@@ -0,0 +1,62 @@
|
||||
import XCTest
|
||||
|
||||
/// Simple test to verify basic app launch and login screen
|
||||
/// This is the foundation test - if this works, we can build more complex tests
|
||||
final class SimpleLoginTest: BaseUITestCase {
|
||||
override var includeResetStateLaunchArgument: Bool { false }
|
||||
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
|
||||
// CRITICAL: Ensure we're logged out before each test
|
||||
ensureLoggedOut()
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
try super.tearDownWithError()
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
/// Ensures the user is logged out and on the login screen
|
||||
private func ensureLoggedOut() {
|
||||
UITestHelpers.ensureLoggedOut(app: app)
|
||||
}
|
||||
|
||||
// MARK: - Tests
|
||||
|
||||
/// Test 1: App launches and shows login screen (or logs out if needed)
|
||||
func testAppLaunchesAndShowsLoginScreen() {
|
||||
// After ensureLoggedOut(), we should be on login screen
|
||||
let welcomeText = app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
|
||||
XCTAssertTrue(welcomeText.exists, "Login screen with 'Welcome Back' text should appear after logout")
|
||||
|
||||
// Also check that we have a username field
|
||||
let usernameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'email'")).firstMatch
|
||||
XCTAssertTrue(usernameField.exists, "Username/email field should exist")
|
||||
}
|
||||
|
||||
/// Test 2: Can type in username and password fields
|
||||
func testCanTypeInLoginFields() {
|
||||
// Already logged out from setUp
|
||||
|
||||
// Find and tap username field
|
||||
let usernameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'email'")).firstMatch
|
||||
XCTAssertTrue(usernameField.waitForExistence(timeout: 10), "Username field should exist")
|
||||
|
||||
usernameField.tap()
|
||||
usernameField.typeText("testuser")
|
||||
|
||||
// Find password field (could be TextField or SecureField)
|
||||
let passwordField = app.secureTextFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'password'")).firstMatch
|
||||
XCTAssertTrue(passwordField.exists, "Password field should exist")
|
||||
|
||||
passwordField.tap()
|
||||
passwordField.typeText("testpass123")
|
||||
|
||||
// Verify we can see a Sign In button
|
||||
let signInButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Sign In'")).firstMatch
|
||||
XCTAssertTrue(signInButton.exists, "Sign In button should exist")
|
||||
}
|
||||
}
|
||||
247
iosApp/CaseraUITests/Suite0_OnboardingTests.swift
Normal file
247
iosApp/CaseraUITests/Suite0_OnboardingTests.swift
Normal file
@@ -0,0 +1,247 @@
|
||||
import XCTest
|
||||
|
||||
/// Onboarding flow tests
|
||||
///
|
||||
/// SETUP REQUIREMENTS:
|
||||
/// This test suite requires the app to be UNINSTALLED before running.
|
||||
/// Add a Pre-action script to the CaseraUITests scheme (Edit Scheme → Test → Pre-actions):
|
||||
/// /usr/bin/xcrun simctl uninstall booted com.tt.casera.CaseraDev
|
||||
/// exit 0
|
||||
///
|
||||
/// There is ONE fresh-install test that runs the complete onboarding flow.
|
||||
/// Additional tests for returning users (login screen) can run without fresh install.
|
||||
final class Suite0_OnboardingTests: BaseUITestCase {
|
||||
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
sleep(2)
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
app.terminate()
|
||||
try super.tearDownWithError()
|
||||
}
|
||||
|
||||
private func typeText(_ text: String, into field: XCUIElement) {
|
||||
field.waitForExistenceOrFail(timeout: 10)
|
||||
for _ in 0..<3 {
|
||||
if !field.isHittable {
|
||||
app.swipeUp()
|
||||
}
|
||||
|
||||
field.forceTap()
|
||||
if !field.hasKeyboardFocus {
|
||||
field.coordinate(withNormalizedOffset: CGVector(dx: 0.8, dy: 0.5)).tap()
|
||||
}
|
||||
if !field.hasKeyboardFocus {
|
||||
continue
|
||||
}
|
||||
|
||||
app.typeText(text)
|
||||
|
||||
if let value = field.value as? String {
|
||||
if value.contains(text) || value.count >= text.count {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
XCTFail("Unable to enter text into \(field)")
|
||||
}
|
||||
|
||||
private func dismissStrongPasswordSuggestionIfPresent() {
|
||||
let chooseOwnPassword = app.buttons["Choose My Own Password"]
|
||||
if chooseOwnPassword.waitForExistence(timeout: 1) {
|
||||
chooseOwnPassword.tap()
|
||||
return
|
||||
}
|
||||
|
||||
let notNow = app.buttons["Not Now"]
|
||||
if notNow.exists && notNow.isHittable {
|
||||
notNow.tap()
|
||||
}
|
||||
}
|
||||
|
||||
private func focusField(_ field: XCUIElement, name: String) {
|
||||
field.waitForExistenceOrFail(timeout: 10)
|
||||
for _ in 0..<4 {
|
||||
if field.hasKeyboardFocus { return }
|
||||
field.forceTap()
|
||||
if field.hasKeyboardFocus { return }
|
||||
field.coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.5)).tap()
|
||||
if field.hasKeyboardFocus { return }
|
||||
}
|
||||
XCTFail("Failed to focus \(name) field")
|
||||
}
|
||||
|
||||
func test_onboarding() {
|
||||
app.activate()
|
||||
sleep(2)
|
||||
|
||||
let springboardApp = XCUIApplication(bundleIdentifier: "com.apple.springboard")
|
||||
let allowButton = springboardApp.buttons["Allow"].firstMatch
|
||||
if allowButton.waitForExistence(timeout: 2) {
|
||||
allowButton.tap()
|
||||
}
|
||||
let welcome = OnboardingWelcomeScreen(app: app)
|
||||
welcome.waitForLoad()
|
||||
welcome.tapStartFresh()
|
||||
|
||||
let valuePropsTitle = app.descendants(matching: .any).matching(identifier: AccessibilityIdentifiers.Onboarding.valuePropsTitle).firstMatch
|
||||
if valuePropsTitle.waitForExistence(timeout: 5) {
|
||||
let valueProps = OnboardingValuePropsScreen(app: app)
|
||||
valueProps.tapContinue()
|
||||
}
|
||||
|
||||
let nameResidenceTitle = app.descendants(matching: .any).matching(identifier: AccessibilityIdentifiers.Onboarding.nameResidenceTitle).firstMatch
|
||||
if nameResidenceTitle.waitForExistence(timeout: 5) {
|
||||
let residenceField = app.textFields[AccessibilityIdentifiers.Onboarding.residenceNameField]
|
||||
residenceField.waitUntilHittable(timeout: 8).tap()
|
||||
residenceField.typeText("xcuitest")
|
||||
app.descendants(matching: .any).matching(identifier: AccessibilityIdentifiers.Onboarding.nameResidenceContinueButton).firstMatch.waitUntilHittable(timeout: 8).tap()
|
||||
}
|
||||
|
||||
let emailExpandButton = app.buttons[AccessibilityIdentifiers.Onboarding.emailSignUpExpandButton].firstMatch
|
||||
if emailExpandButton.waitForExistence(timeout: 10) && emailExpandButton.isHittable {
|
||||
emailExpandButton.tap()
|
||||
}
|
||||
|
||||
let unique = Int(Date().timeIntervalSince1970)
|
||||
let onboardingUsername = "xcuitest\(unique)"
|
||||
let onboardingEmail = "xcuitest_\(unique)@treymail.com"
|
||||
|
||||
let usernameField = app.textFields[AccessibilityIdentifiers.Onboarding.usernameField].firstMatch
|
||||
focusField(usernameField, name: "username")
|
||||
usernameField.typeText(onboardingUsername)
|
||||
XCTAssertTrue((usernameField.value as? String)?.contains(onboardingUsername) == true, "Username should be populated")
|
||||
|
||||
let emailField = app.textFields[AccessibilityIdentifiers.Onboarding.emailField].firstMatch
|
||||
emailField.waitForExistenceOrFail(timeout: 10)
|
||||
var didEnterEmail = false
|
||||
for _ in 0..<5 {
|
||||
app.swipeUp()
|
||||
emailField.forceTap()
|
||||
if emailField.hasKeyboardFocus {
|
||||
emailField.typeText(onboardingEmail)
|
||||
didEnterEmail = true
|
||||
break
|
||||
}
|
||||
}
|
||||
XCTAssertTrue(didEnterEmail, "Email field must become focused for typing")
|
||||
|
||||
let strongPassword = "TestPass123!"
|
||||
let passwordField = app.secureTextFields[AccessibilityIdentifiers.Onboarding.passwordField].firstMatch
|
||||
dismissStrongPasswordSuggestionIfPresent()
|
||||
focusField(passwordField, name: "password")
|
||||
passwordField.typeText(strongPassword)
|
||||
XCTAssertFalse((passwordField.value as? String)?.isEmpty ?? true, "Password should be populated")
|
||||
|
||||
let confirmPasswordField = app.secureTextFields[AccessibilityIdentifiers.Onboarding.confirmPasswordField].firstMatch
|
||||
dismissStrongPasswordSuggestionIfPresent()
|
||||
if !confirmPasswordField.hasKeyboardFocus {
|
||||
app.swipeUp()
|
||||
focusField(confirmPasswordField, name: "confirm password")
|
||||
}
|
||||
confirmPasswordField.typeText(strongPassword)
|
||||
|
||||
let createAccountButtonByID = app.buttons[AccessibilityIdentifiers.Onboarding.createAccountButton]
|
||||
let createAccountButtonByLabel = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Create Account'")).firstMatch
|
||||
let createAccountButton = createAccountButtonByID.exists ? createAccountButtonByID : createAccountButtonByLabel
|
||||
createAccountButton.waitForExistenceOrFail(timeout: 10)
|
||||
if !createAccountButton.isHittable {
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
}
|
||||
if !createAccountButton.isEnabled {
|
||||
// Retry confirm-password input once when validation hasn't propagated.
|
||||
let confirmPasswordField = app.secureTextFields[AccessibilityIdentifiers.Onboarding.confirmPasswordField].firstMatch
|
||||
if confirmPasswordField.waitForExistence(timeout: 3) {
|
||||
focusField(confirmPasswordField, name: "confirm password retry")
|
||||
confirmPasswordField.typeText(strongPassword)
|
||||
}
|
||||
sleep(1)
|
||||
}
|
||||
XCTAssertTrue(createAccountButton.isEnabled, "Create account button should be enabled after valid form entry")
|
||||
createAccountButton.forceTap()
|
||||
|
||||
let verifyCodeField = app.textFields[AccessibilityIdentifiers.Onboarding.verificationCodeField]
|
||||
verifyCodeField.waitForExistenceOrFail(timeout: 12)
|
||||
verifyCodeField.forceTap()
|
||||
app.typeText("123456")
|
||||
|
||||
let verifyButtonByID = app.buttons[AccessibilityIdentifiers.Onboarding.verifyButton]
|
||||
let verifyButtonByLabel = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Verify'")).firstMatch
|
||||
let verifyButton = verifyButtonByID.exists ? verifyButtonByID : verifyButtonByLabel
|
||||
verifyButton.waitForExistenceOrFail(timeout: 10)
|
||||
if !verifyButton.isHittable {
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
}
|
||||
verifyButton.forceTap()
|
||||
|
||||
let addPopular = app.buttons[AccessibilityIdentifiers.Onboarding.addPopularTasksButton].firstMatch
|
||||
if addPopular.waitForExistence(timeout: 10) {
|
||||
addPopular.tap()
|
||||
} else {
|
||||
app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Add Most Popular'")).firstMatch.tap()
|
||||
}
|
||||
|
||||
let addTasksContinue = app.buttons[AccessibilityIdentifiers.Onboarding.addTasksContinueButton].firstMatch
|
||||
if addTasksContinue.waitForExistence(timeout: 10) {
|
||||
addTasksContinue.tap()
|
||||
} else {
|
||||
app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks & Continue'")).firstMatch.tap()
|
||||
}
|
||||
|
||||
let continueWithFree = app.buttons[AccessibilityIdentifiers.Onboarding.continueWithFreeButton].firstMatch
|
||||
if continueWithFree.waitForExistence(timeout: 10) {
|
||||
continueWithFree.tap()
|
||||
} else {
|
||||
app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Continue with Free'")).firstMatch.tap()
|
||||
}
|
||||
|
||||
let residencesHeader = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Your Properties' OR label CONTAINS[c] 'My Properties' OR label CONTAINS[c] 'Residences'")).firstMatch
|
||||
XCTAssertTrue(residencesHeader.waitForExistence(timeout: 5), "Residences list screen must be visible")
|
||||
|
||||
let xcuitestResidence = app.staticTexts["xcuitest"].waitForExistence(timeout: 10)
|
||||
XCTAssertTrue(xcuitestResidence, "Residence should appear in list")
|
||||
|
||||
app/*@START_MENU_TOKEN@*/.images["checkmark.circle.fill"]/*[[".buttons[\"checkmark.circle.fill\"].images",".buttons",".images[\"selected\"]",".images[\"checkmark.circle.fill\"]"],[[[-1,3],[-1,2],[-1,1,1],[-1,0]],[[-1,3],[-1,2]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap()
|
||||
|
||||
let taskOne = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] %@", "HVAC")).firstMatch
|
||||
XCTAssertTrue(taskOne.waitForExistence(timeout: 10), "HVAC task should appear in list")
|
||||
|
||||
let taskTwo = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] %@", "Leaks")).firstMatch
|
||||
XCTAssertTrue(taskTwo.waitForExistence(timeout: 10), "Leaks task should appear in list")
|
||||
|
||||
let taskThree = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] %@", "Coils")).firstMatch
|
||||
XCTAssertTrue(taskThree.waitForExistence(timeout: 10), "Coils task should appear in list")
|
||||
|
||||
|
||||
// Try profile tab logout
|
||||
let profileTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Profile'")).firstMatch
|
||||
if profileTab.exists && profileTab.isHittable {
|
||||
profileTab.tap()
|
||||
|
||||
let logoutButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Logout' OR label CONTAINS[c] 'Log Out'")).firstMatch
|
||||
if logoutButton.waitForExistence(timeout: 3) && logoutButton.isHittable {
|
||||
logoutButton.tap()
|
||||
|
||||
// Handle confirmation alert
|
||||
let alertLogout = app.alerts.buttons["Log Out"]
|
||||
if alertLogout.waitForExistence(timeout: 2) {
|
||||
alertLogout.tap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try verification screen logout
|
||||
let verifyLogout = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Logout'")).firstMatch
|
||||
if verifyLogout.exists && verifyLogout.isHittable {
|
||||
verifyLogout.tap()
|
||||
}
|
||||
|
||||
// Wait for login screen
|
||||
_ = app.textFields[AccessibilityIdentifiers.Authentication.usernameField].waitForExistence(timeout: 8)
|
||||
}
|
||||
}
|
||||
683
iosApp/CaseraUITests/Suite10_ComprehensiveE2ETests.swift
Normal file
683
iosApp/CaseraUITests/Suite10_ComprehensiveE2ETests.swift
Normal file
@@ -0,0 +1,683 @@
|
||||
import XCTest
|
||||
|
||||
/// Comprehensive End-to-End Test Suite
|
||||
/// Closely mirrors TestIntegration_ComprehensiveE2E from myCribAPI-go/internal/integration/integration_test.go
|
||||
///
|
||||
/// This test creates a complete scenario:
|
||||
/// 1. Registers a new user and verifies login
|
||||
/// 2. Creates multiple residences
|
||||
/// 3. Creates multiple tasks in different states
|
||||
/// 4. Verifies task categorization in kanban columns
|
||||
/// 5. Tests task state transitions (in-progress, complete, cancel, archive)
|
||||
///
|
||||
/// IMPORTANT: These are integration tests requiring network connectivity.
|
||||
/// Run against a test/dev server, NOT production.
|
||||
final class Suite10_ComprehensiveE2ETests: BaseUITestCase {
|
||||
override var includeResetStateLaunchArgument: Bool { false }
|
||||
|
||||
|
||||
// Test run identifier for unique data - use static so it's shared across test methods
|
||||
private static let testRunId = Int(Date().timeIntervalSince1970)
|
||||
|
||||
// Test user credentials - unique per test run
|
||||
private var testUsername: String { "e2e_comp_\(Self.testRunId)" }
|
||||
private var testEmail: String { "e2e_comp_\(Self.testRunId)@test.com" }
|
||||
private let testPassword = "TestPass123!"
|
||||
|
||||
/// Fixed verification code used by Go API when DEBUG=true
|
||||
private let verificationCode = "123456"
|
||||
|
||||
/// Track if user has been registered for this test run
|
||||
private static var userRegistered = false
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
|
||||
// Register user on first test, then just ensure logged in for subsequent tests
|
||||
if !Self.userRegistered {
|
||||
registerTestUser()
|
||||
Self.userRegistered = true
|
||||
} else {
|
||||
UITestHelpers.ensureLoggedIn(app: app, username: testUsername, password: testPassword)
|
||||
}
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
try super.tearDownWithError()
|
||||
}
|
||||
|
||||
/// Register a new test user for this test suite
|
||||
private func registerTestUser() {
|
||||
// Check if already logged in
|
||||
let tabBar = app.tabBars.firstMatch
|
||||
if tabBar.exists {
|
||||
return // Already logged in
|
||||
}
|
||||
|
||||
// Check if on login screen, navigate to register
|
||||
let welcomeText = app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
|
||||
if welcomeText.waitForExistence(timeout: 5) {
|
||||
let signUpButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Sign Up'")).firstMatch
|
||||
if signUpButton.exists {
|
||||
signUpButton.tap()
|
||||
sleep(2)
|
||||
}
|
||||
}
|
||||
|
||||
// Fill registration form
|
||||
let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField]
|
||||
if usernameField.waitForExistence(timeout: 5) {
|
||||
usernameField.tap()
|
||||
usernameField.typeText(testUsername)
|
||||
|
||||
let emailField = app.textFields[AccessibilityIdentifiers.Authentication.registerEmailField]
|
||||
emailField.tap()
|
||||
emailField.typeText(testEmail)
|
||||
|
||||
let passwordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerPasswordField]
|
||||
passwordField.tap()
|
||||
dismissStrongPasswordSuggestion()
|
||||
passwordField.typeText(testPassword)
|
||||
|
||||
let confirmPasswordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerConfirmPasswordField]
|
||||
confirmPasswordField.tap()
|
||||
dismissStrongPasswordSuggestion()
|
||||
confirmPasswordField.typeText(testPassword)
|
||||
|
||||
dismissKeyboard()
|
||||
sleep(1)
|
||||
|
||||
// Submit registration
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
|
||||
var registerButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton]
|
||||
if !registerButton.exists || !registerButton.isHittable {
|
||||
registerButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Create Account' OR label CONTAINS[c] 'Register'")).firstMatch
|
||||
}
|
||||
if registerButton.exists {
|
||||
registerButton.tap()
|
||||
sleep(3)
|
||||
}
|
||||
|
||||
// Handle email verification
|
||||
let verifyEmailTitle = app.staticTexts["Verify Your Email"]
|
||||
if verifyEmailTitle.waitForExistence(timeout: 10) {
|
||||
let codeField = app.textFields[AccessibilityIdentifiers.Authentication.verificationCodeField]
|
||||
if codeField.waitForExistence(timeout: 5) {
|
||||
codeField.tap()
|
||||
codeField.typeText(verificationCode)
|
||||
sleep(5)
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for login to complete
|
||||
_ = tabBar.waitForExistence(timeout: 15)
|
||||
}
|
||||
}
|
||||
|
||||
/// Dismiss strong password suggestion if shown
|
||||
private func dismissStrongPasswordSuggestion() {
|
||||
let chooseOwnPassword = app.buttons["Choose My Own Password"]
|
||||
if chooseOwnPassword.waitForExistence(timeout: 1) {
|
||||
chooseOwnPassword.tap()
|
||||
return
|
||||
}
|
||||
let notNow = app.buttons["Not Now"]
|
||||
if notNow.exists && notNow.isHittable {
|
||||
notNow.tap()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
private func navigateToTab(_ tabName: String) {
|
||||
let tab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] '\(tabName)'")).firstMatch
|
||||
if tab.waitForExistence(timeout: 5) && !tab.isSelected {
|
||||
tab.tap()
|
||||
sleep(2)
|
||||
}
|
||||
}
|
||||
|
||||
/// Dismiss keyboard by tapping outside (doesn't submit forms)
|
||||
private func dismissKeyboard() {
|
||||
// Tap on a neutral area to dismiss keyboard without submitting
|
||||
let coordinate = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.1))
|
||||
coordinate.tap()
|
||||
Thread.sleep(forTimeInterval: 0.5)
|
||||
}
|
||||
|
||||
/// Creates a residence with the given name
|
||||
/// Returns true if successful
|
||||
@discardableResult
|
||||
private func createResidence(name: String, streetAddress: String = "123 Test St", city: String = "Austin", state: String = "TX", postalCode: String = "78701") -> Bool {
|
||||
navigateToTab("Residences")
|
||||
sleep(2)
|
||||
|
||||
let addButton = app.buttons[AccessibilityIdentifiers.Residence.addButton]
|
||||
guard addButton.waitForExistence(timeout: 5) else {
|
||||
XCTFail("Add residence button not found")
|
||||
return false
|
||||
}
|
||||
addButton.tap()
|
||||
sleep(2)
|
||||
|
||||
// Fill name
|
||||
let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Name'")).firstMatch
|
||||
guard nameField.waitForExistence(timeout: 5) else {
|
||||
XCTFail("Name field not found")
|
||||
return false
|
||||
}
|
||||
nameField.tap()
|
||||
nameField.typeText(name)
|
||||
|
||||
// Fill address
|
||||
fillTextField(placeholder: "Street", text: streetAddress)
|
||||
fillTextField(placeholder: "City", text: city)
|
||||
fillTextField(placeholder: "State", text: state)
|
||||
fillTextField(placeholder: "Postal", text: postalCode)
|
||||
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
|
||||
// Save
|
||||
let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch
|
||||
guard saveButton.exists else {
|
||||
XCTFail("Save button not found")
|
||||
return false
|
||||
}
|
||||
saveButton.tap()
|
||||
sleep(3)
|
||||
|
||||
// Verify created
|
||||
let residenceCard = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(name)'")).firstMatch
|
||||
return residenceCard.waitForExistence(timeout: 10)
|
||||
}
|
||||
|
||||
/// Creates a task with the given title
|
||||
/// Returns true if successful
|
||||
@discardableResult
|
||||
private func createTask(title: String, description: String? = nil) -> Bool {
|
||||
navigateToTab("Tasks")
|
||||
sleep(2)
|
||||
|
||||
let addButton = findAddTaskButton()
|
||||
guard addButton.waitForExistence(timeout: 5) && addButton.isEnabled else {
|
||||
XCTFail("Add task button not found or disabled")
|
||||
return false
|
||||
}
|
||||
addButton.tap()
|
||||
sleep(2)
|
||||
|
||||
// Fill title
|
||||
let titleField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Title'")).firstMatch
|
||||
guard titleField.waitForExistence(timeout: 5) else {
|
||||
XCTFail("Title field not found")
|
||||
return false
|
||||
}
|
||||
titleField.tap()
|
||||
titleField.typeText(title)
|
||||
|
||||
// Fill description if provided
|
||||
if let desc = description {
|
||||
let descField = app.textViews.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Description'")).firstMatch
|
||||
if descField.exists {
|
||||
descField.tap()
|
||||
descField.typeText(desc)
|
||||
}
|
||||
}
|
||||
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
|
||||
// Save
|
||||
let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch
|
||||
guard saveButton.exists else {
|
||||
XCTFail("Save button not found")
|
||||
return false
|
||||
}
|
||||
saveButton.tap()
|
||||
sleep(3)
|
||||
|
||||
// Verify created
|
||||
let taskCard = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(title)'")).firstMatch
|
||||
return taskCard.waitForExistence(timeout: 10)
|
||||
}
|
||||
|
||||
private func fillTextField(placeholder: String, text: String) {
|
||||
let field = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] '\(placeholder)'")).firstMatch
|
||||
if field.exists {
|
||||
field.tap()
|
||||
field.typeText(text)
|
||||
}
|
||||
}
|
||||
|
||||
private func findAddTaskButton() -> XCUIElement {
|
||||
// Strategy 1: Accessibility identifier
|
||||
let addButtonById = app.buttons[AccessibilityIdentifiers.Task.addButton].firstMatch
|
||||
if addButtonById.exists && addButtonById.isEnabled {
|
||||
return addButtonById
|
||||
}
|
||||
|
||||
// Strategy 2: Navigation bar plus button
|
||||
let navBarButtons = app.navigationBars.buttons
|
||||
for i in 0..<navBarButtons.count {
|
||||
let button = navBarButtons.element(boundBy: i)
|
||||
if (button.label == "plus" || button.label.contains("Add")) && button.isEnabled {
|
||||
return button
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 3: Empty state button
|
||||
let emptyStateButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Add Task'")).firstMatch
|
||||
if emptyStateButton.exists && emptyStateButton.isEnabled {
|
||||
return emptyStateButton
|
||||
}
|
||||
|
||||
return addButtonById
|
||||
}
|
||||
|
||||
// MARK: - Test 1: Create Multiple Residences
|
||||
// Phase 2 of TestIntegration_ComprehensiveE2E
|
||||
|
||||
func test01_createMultipleResidences() {
|
||||
let residenceNames = [
|
||||
"E2E Main House \(Self.testRunId)",
|
||||
"E2E Beach House \(Self.testRunId)",
|
||||
"E2E Mountain Cabin \(Self.testRunId)"
|
||||
]
|
||||
|
||||
for (index, name) in residenceNames.enumerated() {
|
||||
let streetAddress = "\(100 * (index + 1)) Test St"
|
||||
let success = createResidence(name: name, streetAddress: streetAddress)
|
||||
XCTAssertTrue(success, "Should create residence: \(name)")
|
||||
}
|
||||
|
||||
// Verify all residences exist
|
||||
navigateToTab("Residences")
|
||||
sleep(2)
|
||||
|
||||
for name in residenceNames {
|
||||
let residenceCard = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(name)'")).firstMatch
|
||||
XCTAssertTrue(residenceCard.waitForExistence(timeout: 5), "Residence '\(name)' should exist in list")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Test 2: Create Tasks with Various States
|
||||
// Phase 3 of TestIntegration_ComprehensiveE2E
|
||||
|
||||
func test02_createTasksWithVariousStates() {
|
||||
// Ensure at least one residence exists
|
||||
navigateToTab("Residences")
|
||||
sleep(2)
|
||||
|
||||
let emptyState = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'No properties'")).firstMatch
|
||||
if emptyState.exists {
|
||||
createResidence(name: "Task Test Residence \(Self.testRunId)")
|
||||
}
|
||||
|
||||
// Create tasks with different purposes
|
||||
let tasks = [
|
||||
("E2E Active Task \(Self.testRunId)", "Task that remains active"),
|
||||
("E2E Progress Task \(Self.testRunId)", "Task to mark in-progress"),
|
||||
("E2E Complete Task \(Self.testRunId)", "Task to complete"),
|
||||
("E2E Cancel Task \(Self.testRunId)", "Task to cancel")
|
||||
]
|
||||
|
||||
for (title, description) in tasks {
|
||||
let success = createTask(title: title, description: description)
|
||||
XCTAssertTrue(success, "Should create task: \(title)")
|
||||
}
|
||||
|
||||
// Verify all tasks exist
|
||||
navigateToTab("Tasks")
|
||||
sleep(2)
|
||||
|
||||
for (title, _) in tasks {
|
||||
let taskCard = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(title)'")).firstMatch
|
||||
XCTAssertTrue(taskCard.waitForExistence(timeout: 5), "Task '\(title)' should exist")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Test 3: Task State Transitions
|
||||
// Mirrors task operations from TestIntegration_TaskFlow
|
||||
|
||||
func test03_taskStateTransitions() {
|
||||
navigateToTab("Tasks")
|
||||
sleep(2)
|
||||
|
||||
// Find a task to transition (create one if needed)
|
||||
let testTaskTitle = "E2E State Test \(Self.testRunId)"
|
||||
|
||||
var taskExists = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(testTaskTitle)'")).firstMatch.exists
|
||||
if !taskExists {
|
||||
// Check if any residence exists first
|
||||
navigateToTab("Residences")
|
||||
sleep(2)
|
||||
|
||||
let emptyResidences = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'No properties'")).firstMatch
|
||||
if emptyResidences.exists {
|
||||
createResidence(name: "State Test Residence \(Self.testRunId)")
|
||||
}
|
||||
|
||||
createTask(title: testTaskTitle, description: "Testing state transitions")
|
||||
navigateToTab("Tasks")
|
||||
sleep(2)
|
||||
}
|
||||
|
||||
// Find and tap the task
|
||||
let taskCard = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(testTaskTitle)'")).firstMatch
|
||||
if taskCard.waitForExistence(timeout: 5) {
|
||||
taskCard.tap()
|
||||
sleep(2)
|
||||
|
||||
// Try to mark in progress
|
||||
let inProgressButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'In Progress' OR label CONTAINS[c] 'Start'")).firstMatch
|
||||
if inProgressButton.exists && inProgressButton.isEnabled {
|
||||
inProgressButton.tap()
|
||||
sleep(2)
|
||||
}
|
||||
|
||||
// Try to complete
|
||||
let completeButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Complete' OR label CONTAINS[c] 'Mark Complete'")).firstMatch
|
||||
if completeButton.exists && completeButton.isEnabled {
|
||||
completeButton.tap()
|
||||
sleep(2)
|
||||
|
||||
// Handle completion form if shown
|
||||
let submitButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Submit' OR label CONTAINS[c] 'Save'")).firstMatch
|
||||
if submitButton.waitForExistence(timeout: 2) {
|
||||
submitButton.tap()
|
||||
sleep(2)
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate back
|
||||
let backButton = app.navigationBars.buttons.element(boundBy: 0)
|
||||
if backButton.exists && backButton.isHittable {
|
||||
backButton.tap()
|
||||
sleep(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Test 4: Task Cancel Operation
|
||||
|
||||
func test04_taskCancelOperation() {
|
||||
navigateToTab("Tasks")
|
||||
sleep(2)
|
||||
|
||||
let testTaskTitle = "E2E Cancel Test \(Self.testRunId)"
|
||||
|
||||
// Create task if doesn't exist
|
||||
if !app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(testTaskTitle)'")).firstMatch.exists {
|
||||
navigateToTab("Residences")
|
||||
sleep(1)
|
||||
|
||||
let emptyResidences = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'No properties'")).firstMatch
|
||||
if emptyResidences.exists {
|
||||
createResidence(name: "Cancel Test Residence \(Self.testRunId)")
|
||||
}
|
||||
|
||||
createTask(title: testTaskTitle, description: "Task to be cancelled")
|
||||
navigateToTab("Tasks")
|
||||
sleep(2)
|
||||
}
|
||||
|
||||
// Find and tap task
|
||||
let taskCard = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(testTaskTitle)'")).firstMatch
|
||||
if taskCard.waitForExistence(timeout: 5) {
|
||||
taskCard.tap()
|
||||
sleep(2)
|
||||
|
||||
// Look for cancel button
|
||||
let cancelButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Cancel Task' OR label CONTAINS[c] 'Cancel'")).firstMatch
|
||||
if cancelButton.exists && cancelButton.isEnabled {
|
||||
cancelButton.tap()
|
||||
sleep(1)
|
||||
|
||||
// Confirm cancellation if alert shown
|
||||
let confirmButton = app.alerts.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Cancel' OR label CONTAINS[c] 'Confirm' OR label CONTAINS[c] 'Yes'")).firstMatch
|
||||
if confirmButton.exists {
|
||||
confirmButton.tap()
|
||||
sleep(2)
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate back
|
||||
let backButton = app.navigationBars.buttons.element(boundBy: 0)
|
||||
if backButton.exists && backButton.isHittable {
|
||||
backButton.tap()
|
||||
sleep(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Test 5: Task Archive Operation
|
||||
|
||||
func test05_taskArchiveOperation() {
|
||||
navigateToTab("Tasks")
|
||||
sleep(2)
|
||||
|
||||
let testTaskTitle = "E2E Archive Test \(Self.testRunId)"
|
||||
|
||||
// Create task if doesn't exist
|
||||
if !app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(testTaskTitle)'")).firstMatch.exists {
|
||||
navigateToTab("Residences")
|
||||
sleep(1)
|
||||
|
||||
let emptyResidences = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'No properties'")).firstMatch
|
||||
if emptyResidences.exists {
|
||||
createResidence(name: "Archive Test Residence \(Self.testRunId)")
|
||||
}
|
||||
|
||||
createTask(title: testTaskTitle, description: "Task to be archived")
|
||||
navigateToTab("Tasks")
|
||||
sleep(2)
|
||||
}
|
||||
|
||||
// Find and tap task
|
||||
let taskCard = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(testTaskTitle)'")).firstMatch
|
||||
if taskCard.waitForExistence(timeout: 5) {
|
||||
taskCard.tap()
|
||||
sleep(2)
|
||||
|
||||
// Look for archive button
|
||||
let archiveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Archive'")).firstMatch
|
||||
if archiveButton.exists && archiveButton.isEnabled {
|
||||
archiveButton.tap()
|
||||
sleep(1)
|
||||
|
||||
// Confirm archive if alert shown
|
||||
let confirmButton = app.alerts.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Archive' OR label CONTAINS[c] 'Confirm' OR label CONTAINS[c] 'Yes'")).firstMatch
|
||||
if confirmButton.exists {
|
||||
confirmButton.tap()
|
||||
sleep(2)
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate back
|
||||
let backButton = app.navigationBars.buttons.element(boundBy: 0)
|
||||
if backButton.exists && backButton.isHittable {
|
||||
backButton.tap()
|
||||
sleep(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Test 6: Verify Kanban Column Structure
|
||||
// Phase 6 of TestIntegration_ComprehensiveE2E
|
||||
|
||||
func test06_verifyKanbanStructure() {
|
||||
navigateToTab("Tasks")
|
||||
sleep(3)
|
||||
|
||||
// Expected kanban column names (may vary by implementation)
|
||||
let expectedColumns = [
|
||||
"Overdue",
|
||||
"In Progress",
|
||||
"Due Soon",
|
||||
"Upcoming",
|
||||
"Completed",
|
||||
"Cancelled"
|
||||
]
|
||||
|
||||
var foundColumns: [String] = []
|
||||
|
||||
for column in expectedColumns {
|
||||
let columnHeader = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] '\(column)'")).firstMatch
|
||||
if columnHeader.exists {
|
||||
foundColumns.append(column)
|
||||
}
|
||||
}
|
||||
|
||||
// Should have at least some kanban columns OR be in list view
|
||||
let hasKanbanView = foundColumns.count >= 2
|
||||
let hasListView = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks' OR label CONTAINS[c] 'All Tasks'")).firstMatch.exists
|
||||
|
||||
XCTAssertTrue(hasKanbanView || hasListView, "Should display tasks in kanban or list view. Found columns: \(foundColumns)")
|
||||
}
|
||||
|
||||
// MARK: - Test 7: Residence Details Show Tasks
|
||||
// Verifies that residence detail screen shows associated tasks
|
||||
|
||||
func test07_residenceDetailsShowTasks() {
|
||||
navigateToTab("Residences")
|
||||
sleep(2)
|
||||
|
||||
// Find any residence
|
||||
let residenceCard = app.cells.firstMatch
|
||||
guard residenceCard.waitForExistence(timeout: 5) else {
|
||||
// No residences - create one with a task
|
||||
createResidence(name: "Detail Test Residence \(Self.testRunId)")
|
||||
createTask(title: "Detail Test Task \(Self.testRunId)")
|
||||
navigateToTab("Residences")
|
||||
sleep(2)
|
||||
|
||||
let newResidenceCard = app.cells.firstMatch
|
||||
guard newResidenceCard.waitForExistence(timeout: 5) else {
|
||||
XCTFail("Could not find any residence")
|
||||
return
|
||||
}
|
||||
newResidenceCard.tap()
|
||||
sleep(2)
|
||||
return
|
||||
}
|
||||
|
||||
residenceCard.tap()
|
||||
sleep(2)
|
||||
|
||||
// Look for tasks section in residence details
|
||||
let tasksSection = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks' OR label CONTAINS[c] 'Maintenance'")).firstMatch
|
||||
let taskCount = app.staticTexts.containing(NSPredicate(format: "label MATCHES '\\\\d+ tasks?' OR label MATCHES '\\\\d+ Tasks?'")).firstMatch
|
||||
|
||||
// Either tasks section header or task count should be visible
|
||||
let hasTasksInfo = tasksSection.exists || taskCount.exists
|
||||
|
||||
// Navigate back
|
||||
let backButton = app.navigationBars.buttons.element(boundBy: 0)
|
||||
if backButton.exists && backButton.isHittable {
|
||||
backButton.tap()
|
||||
sleep(1)
|
||||
}
|
||||
|
||||
// Note: Not asserting because task section visibility depends on UI design
|
||||
}
|
||||
|
||||
// MARK: - Test 8: Contractor CRUD (Mirrors backend contractor tests)
|
||||
|
||||
func test08_contractorCRUD() {
|
||||
navigateToTab("Contractors")
|
||||
sleep(2)
|
||||
|
||||
let contractorName = "E2E Test Contractor \(Self.testRunId)"
|
||||
|
||||
// Check if Contractors tab exists
|
||||
let contractorsTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Contractors'")).firstMatch
|
||||
guard contractorsTab.exists else {
|
||||
// Contractors may not be a main tab - skip this test
|
||||
return
|
||||
}
|
||||
|
||||
// Try to add contractor
|
||||
let addButton = app.buttons[AccessibilityIdentifiers.Contractor.addButton]
|
||||
guard addButton.waitForExistence(timeout: 5) else {
|
||||
// May need residence first
|
||||
return
|
||||
}
|
||||
|
||||
addButton.tap()
|
||||
sleep(2)
|
||||
|
||||
// Fill contractor form
|
||||
let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Name'")).firstMatch
|
||||
if nameField.exists {
|
||||
nameField.tap()
|
||||
nameField.typeText(contractorName)
|
||||
|
||||
let companyField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Company'")).firstMatch
|
||||
if companyField.exists {
|
||||
companyField.tap()
|
||||
companyField.typeText("Test Company Inc")
|
||||
}
|
||||
|
||||
let phoneField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Phone'")).firstMatch
|
||||
if phoneField.exists {
|
||||
phoneField.tap()
|
||||
phoneField.typeText("555-123-4567")
|
||||
}
|
||||
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
|
||||
let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch
|
||||
if saveButton.exists {
|
||||
saveButton.tap()
|
||||
sleep(3)
|
||||
|
||||
// Verify contractor was created
|
||||
let contractorCard = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(contractorName)'")).firstMatch
|
||||
XCTAssertTrue(contractorCard.waitForExistence(timeout: 10), "Contractor '\(contractorName)' should be created")
|
||||
}
|
||||
} else {
|
||||
// Cancel if form didn't load properly
|
||||
let cancelButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Cancel'")).firstMatch
|
||||
if cancelButton.exists {
|
||||
cancelButton.tap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Test 9: Full Flow Summary
|
||||
|
||||
func test09_fullFlowSummary() {
|
||||
// This test verifies the overall app state after running previous tests
|
||||
|
||||
// Check Residences tab
|
||||
navigateToTab("Residences")
|
||||
sleep(2)
|
||||
|
||||
let residencesList = app.cells
|
||||
let residenceCount = residencesList.count
|
||||
|
||||
// Check Tasks tab
|
||||
navigateToTab("Tasks")
|
||||
sleep(2)
|
||||
|
||||
let tasksScreen = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch
|
||||
XCTAssertTrue(tasksScreen.exists, "Tasks screen should be accessible")
|
||||
|
||||
// Check Profile tab
|
||||
navigateToTab("Profile")
|
||||
sleep(2)
|
||||
|
||||
let logoutButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Logout' OR label CONTAINS[c] 'Log Out'")).firstMatch
|
||||
XCTAssertTrue(logoutButton.exists, "User should be logged in with logout option available")
|
||||
|
||||
print("=== E2E Test Summary ===")
|
||||
print("Residences found: \(residenceCount)")
|
||||
print("Tasks screen accessible: true")
|
||||
print("User logged in: true")
|
||||
print("========================")
|
||||
}
|
||||
}
|
||||
654
iosApp/CaseraUITests/Suite1_RegistrationTests.swift
Normal file
654
iosApp/CaseraUITests/Suite1_RegistrationTests.swift
Normal file
@@ -0,0 +1,654 @@
|
||||
import XCTest
|
||||
|
||||
/// Comprehensive registration flow tests with strict, failure-first assertions
|
||||
/// Tests verify both positive AND negative conditions to ensure robust validation
|
||||
final class Suite1_RegistrationTests: BaseUITestCase {
|
||||
override var includeResetStateLaunchArgument: Bool { false }
|
||||
|
||||
|
||||
// Test user credentials - using timestamp to ensure unique users
|
||||
private var testUsername: String {
|
||||
return "testuser_\(Int(Date().timeIntervalSince1970))"
|
||||
}
|
||||
private var testEmail: String {
|
||||
return "test_\(Int(Date().timeIntervalSince1970))@example.com"
|
||||
}
|
||||
private let testPassword = "Pass1234"
|
||||
|
||||
/// Fixed test verification code - Go API uses this code when DEBUG=true
|
||||
private let testVerificationCode = "123456"
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
|
||||
// STRICT: Verify app launched to a known state
|
||||
let loginScreen = app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
|
||||
|
||||
// If login isn't visible, force deterministic navigation to login.
|
||||
if !loginScreen.waitForExistence(timeout: 3) {
|
||||
ensureLoggedOut()
|
||||
}
|
||||
|
||||
// STRICT: Must be on login screen before each test
|
||||
XCTAssertTrue(loginScreen.waitForExistence(timeout: 10), "PRECONDITION FAILED: Must start on login screen")
|
||||
|
||||
app.swipeUp()
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
ensureLoggedOut()
|
||||
try super.tearDownWithError()
|
||||
}
|
||||
|
||||
// MARK: - Strict Helper Methods
|
||||
|
||||
private func ensureLoggedOut() {
|
||||
UITestHelpers.ensureLoggedOut(app: app)
|
||||
}
|
||||
|
||||
/// Navigate to registration screen with strict verification
|
||||
/// Note: Registration is presented as a sheet, so login screen elements still exist underneath
|
||||
private func navigateToRegistration() {
|
||||
app.swipeUp()
|
||||
// PRECONDITION: Must be on login screen
|
||||
let welcomeText = app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
|
||||
XCTAssertTrue(welcomeText.exists, "PRECONDITION: Must be on login screen to navigate to registration")
|
||||
|
||||
let signUpButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Sign Up'")).firstMatch
|
||||
XCTAssertTrue(signUpButton.waitForExistence(timeout: 5), "Sign Up button must exist on login screen")
|
||||
XCTAssertTrue(signUpButton.isHittable, "Sign Up button must be tappable")
|
||||
|
||||
dismissKeyboard()
|
||||
signUpButton.tap()
|
||||
|
||||
// STRICT: Verify registration screen appeared (shown as sheet)
|
||||
// Note: Login screen still exists underneath the sheet, so we verify registration elements instead
|
||||
let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField]
|
||||
XCTAssertTrue(usernameField.waitForExistence(timeout: 5), "Registration username field must appear")
|
||||
XCTAssertTrue(waitForElementToBeHittable(usernameField, timeout: 5), "Registration username field must be tappable")
|
||||
|
||||
// Keep action buttons visible for strict assertions and interactions.
|
||||
let createAccountButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton]
|
||||
if createAccountButton.exists && !createAccountButton.isHittable {
|
||||
let scrollView = app.scrollViews.firstMatch
|
||||
if scrollView.exists {
|
||||
createAccountButton.scrollIntoView(in: scrollView, maxSwipes: 5)
|
||||
}
|
||||
}
|
||||
|
||||
// STRICT: The Sign Up button should no longer be hittable (covered by sheet)
|
||||
XCTAssertFalse(signUpButton.isHittable, "Login Sign Up button should be covered by registration sheet")
|
||||
}
|
||||
|
||||
/// Dismisses iOS Strong Password suggestion overlay
|
||||
private func dismissStrongPasswordSuggestion() {
|
||||
let chooseOwnPassword = app.buttons["Choose My Own Password"]
|
||||
if chooseOwnPassword.waitForExistence(timeout: 1) {
|
||||
chooseOwnPassword.tap()
|
||||
return
|
||||
}
|
||||
|
||||
let notNowButton = app.buttons["Not Now"]
|
||||
if notNowButton.exists && notNowButton.isHittable {
|
||||
notNowButton.tap()
|
||||
return
|
||||
}
|
||||
|
||||
// Dismiss by tapping elsewhere
|
||||
let strongPasswordText = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Strong Password'")).firstMatch
|
||||
if strongPasswordText.exists {
|
||||
app.tap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait for element to disappear - CRITICAL for strict testing
|
||||
private func waitForElementToDisappear(_ element: XCUIElement, timeout: TimeInterval) -> Bool {
|
||||
let expectation = XCTNSPredicateExpectation(
|
||||
predicate: NSPredicate(format: "exists == false"),
|
||||
object: element
|
||||
)
|
||||
let result = XCTWaiter.wait(for: [expectation], timeout: timeout)
|
||||
return result == .completed
|
||||
}
|
||||
|
||||
/// Wait for element to become hittable (visible AND interactive)
|
||||
private func waitForElementToBeHittable(_ element: XCUIElement, timeout: TimeInterval) -> Bool {
|
||||
let expectation = XCTNSPredicateExpectation(
|
||||
predicate: NSPredicate(format: "isHittable == true"),
|
||||
object: element
|
||||
)
|
||||
let result = XCTWaiter.wait(for: [expectation], timeout: timeout)
|
||||
return result == .completed
|
||||
}
|
||||
|
||||
/// Verification screen readiness check based on stable accessibility IDs.
|
||||
private func waitForVerificationScreen(timeout: TimeInterval) -> Bool {
|
||||
let authCodeField = app.textFields[AccessibilityIdentifiers.Authentication.verificationCodeField]
|
||||
let onboardingCodeField = app.textFields[AccessibilityIdentifiers.Onboarding.verificationCodeField]
|
||||
let authVerifyButton = app.buttons[AccessibilityIdentifiers.Authentication.verifyButton]
|
||||
let onboardingVerifyButton = app.buttons[AccessibilityIdentifiers.Onboarding.verifyButton]
|
||||
return authCodeField.waitForExistence(timeout: timeout)
|
||||
|| onboardingCodeField.waitForExistence(timeout: timeout)
|
||||
|| authVerifyButton.waitForExistence(timeout: timeout)
|
||||
|| onboardingVerifyButton.waitForExistence(timeout: timeout)
|
||||
}
|
||||
|
||||
private func verificationCodeField() -> XCUIElement {
|
||||
let authCodeField = app.textFields[AccessibilityIdentifiers.Authentication.verificationCodeField]
|
||||
if authCodeField.exists {
|
||||
return authCodeField
|
||||
}
|
||||
return app.textFields[AccessibilityIdentifiers.Onboarding.verificationCodeField]
|
||||
}
|
||||
|
||||
private func verificationButton() -> XCUIElement {
|
||||
let authVerifyButton = app.buttons[AccessibilityIdentifiers.Authentication.verifyButton]
|
||||
if authVerifyButton.exists {
|
||||
return authVerifyButton
|
||||
}
|
||||
let onboardingVerifyButton = app.buttons[AccessibilityIdentifiers.Onboarding.verifyButton]
|
||||
if onboardingVerifyButton.exists {
|
||||
return onboardingVerifyButton
|
||||
}
|
||||
return app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Verify'")).firstMatch
|
||||
}
|
||||
|
||||
/// Dismiss keyboard by swiping down on the keyboard area
|
||||
private func dismissKeyboard() {
|
||||
let app = XCUIApplication()
|
||||
if app.keys.element(boundBy: 0).exists {
|
||||
app.typeText("\n")
|
||||
}
|
||||
|
||||
// Give a moment for keyboard to dismiss
|
||||
Thread.sleep(forTimeInterval: 2)
|
||||
}
|
||||
|
||||
/// Fill registration form with given credentials
|
||||
private func fillRegistrationForm(username: String, email: String, password: String, confirmPassword: String) {
|
||||
let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField]
|
||||
let emailField = app.textFields[AccessibilityIdentifiers.Authentication.registerEmailField]
|
||||
let passwordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerPasswordField]
|
||||
let confirmPasswordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerConfirmPasswordField]
|
||||
|
||||
// STRICT: All fields must exist and be hittable
|
||||
XCTAssertTrue(usernameField.isHittable, "Username field must be hittable")
|
||||
XCTAssertTrue(emailField.isHittable, "Email field must be hittable")
|
||||
XCTAssertTrue(passwordField.isHittable, "Password field must be hittable")
|
||||
XCTAssertTrue(confirmPasswordField.isHittable, "Confirm password field must be hittable")
|
||||
|
||||
usernameField.tap()
|
||||
usernameField.typeText(username)
|
||||
|
||||
emailField.tap()
|
||||
emailField.typeText(email)
|
||||
|
||||
passwordField.tap()
|
||||
dismissStrongPasswordSuggestion()
|
||||
passwordField.typeText(password)
|
||||
|
||||
confirmPasswordField.tap()
|
||||
dismissStrongPasswordSuggestion()
|
||||
confirmPasswordField.typeText(confirmPassword)
|
||||
|
||||
// Dismiss keyboard after filling form so buttons are accessible
|
||||
dismissKeyboard()
|
||||
}
|
||||
|
||||
// MARK: - 1. UI/Element Tests (no backend, pure UI verification)
|
||||
|
||||
func test01_registrationScreenElements() {
|
||||
navigateToRegistration()
|
||||
|
||||
// STRICT: All form elements must exist AND be hittable
|
||||
let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField]
|
||||
let emailField = app.textFields[AccessibilityIdentifiers.Authentication.registerEmailField]
|
||||
let passwordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerPasswordField]
|
||||
let confirmPasswordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerConfirmPasswordField]
|
||||
let createAccountButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton]
|
||||
let cancelButton = app.buttons[AccessibilityIdentifiers.Authentication.registerCancelButton]
|
||||
|
||||
XCTAssertTrue(usernameField.exists && usernameField.isHittable, "Username field must be visible and tappable")
|
||||
XCTAssertTrue(emailField.exists && emailField.isHittable, "Email field must be visible and tappable")
|
||||
XCTAssertTrue(passwordField.exists && passwordField.isHittable, "Password field must be visible and tappable")
|
||||
XCTAssertTrue(confirmPasswordField.exists && confirmPasswordField.isHittable, "Confirm password field must be visible and tappable")
|
||||
XCTAssertTrue(createAccountButton.exists && createAccountButton.isHittable, "Create Account button must be visible and tappable")
|
||||
XCTAssertTrue(cancelButton.exists && cancelButton.isHittable, "Cancel button must be visible and tappable")
|
||||
|
||||
// NEGATIVE CHECK: Should NOT see verification screen elements as hittable
|
||||
let verifyTitle = app.staticTexts["Verify Your Email"]
|
||||
XCTAssertFalse(verifyTitle.exists && verifyTitle.isHittable, "Verification screen should NOT be visible on registration form")
|
||||
|
||||
// NEGATIVE CHECK: Login Sign Up button should not be hittable (covered by sheet)
|
||||
let loginSignUpButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Sign Up'")).firstMatch
|
||||
// Note: The button might still exist but should not be hittable due to sheet coverage
|
||||
if loginSignUpButton.exists {
|
||||
XCTAssertFalse(loginSignUpButton.isHittable, "Login screen's Sign Up button should be covered by registration sheet")
|
||||
}
|
||||
}
|
||||
|
||||
func test02_cancelRegistration() {
|
||||
navigateToRegistration()
|
||||
|
||||
// Capture that we're on registration screen
|
||||
let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField]
|
||||
XCTAssertTrue(usernameField.isHittable, "PRECONDITION: Must be on registration screen")
|
||||
|
||||
let cancelButton = app.buttons[AccessibilityIdentifiers.Authentication.registerCancelButton]
|
||||
XCTAssertTrue(cancelButton.isHittable, "Cancel button must be tappable")
|
||||
dismissKeyboard()
|
||||
cancelButton.tap()
|
||||
|
||||
// STRICT: Registration sheet must dismiss - username field should no longer be hittable
|
||||
XCTAssertTrue(waitForElementToDisappear(usernameField, timeout: 5), "Registration form must disappear after cancel")
|
||||
|
||||
// STRICT: Login screen must now be interactive again
|
||||
let welcomeText = app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
|
||||
XCTAssertTrue(welcomeText.waitForExistence(timeout: 5), "Login screen must be visible after cancel")
|
||||
|
||||
// STRICT: Sign Up button should be hittable again (sheet dismissed)
|
||||
let signUpButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Sign Up'")).firstMatch
|
||||
XCTAssertTrue(waitForElementToBeHittable(signUpButton, timeout: 5), "Sign Up button must be tappable after cancel")
|
||||
}
|
||||
|
||||
// MARK: - 2. Client-Side Validation Tests (no API calls, fail locally)
|
||||
|
||||
func test03_registrationWithEmptyFields() {
|
||||
navigateToRegistration()
|
||||
|
||||
let createAccountButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton]
|
||||
XCTAssertTrue(createAccountButton.isHittable, "Create Account button must be tappable")
|
||||
|
||||
// Capture current state
|
||||
let verifyTitle = app.staticTexts["Verify Your Email"]
|
||||
XCTAssertFalse(verifyTitle.exists, "PRECONDITION: Should not be on verification screen")
|
||||
|
||||
dismissKeyboard()
|
||||
createAccountButton.tap()
|
||||
|
||||
// STRICT: Must show error message
|
||||
let errorPredicate = NSPredicate(format: "label CONTAINS[c] 'required' OR label CONTAINS[c] 'Username'")
|
||||
let errorMessage = app.staticTexts.containing(errorPredicate).firstMatch
|
||||
XCTAssertTrue(errorMessage.waitForExistence(timeout: 3), "Error message must appear for empty fields")
|
||||
|
||||
// NEGATIVE CHECK: Should NOT navigate away from registration
|
||||
// XCTAssertFalse(verifyTitle.exists, "Should NOT navigate to verification screen with empty fields")
|
||||
|
||||
// STRICT: Registration form should still be visible and interactive
|
||||
// let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField]
|
||||
// XCTAssertTrue(usernameField.isHittable, "Username field should still be tappable after error")
|
||||
}
|
||||
|
||||
func test04_registrationWithInvalidEmail() {
|
||||
navigateToRegistration()
|
||||
|
||||
fillRegistrationForm(
|
||||
username: "testuser",
|
||||
email: "invalid-email", // Invalid format
|
||||
password: testPassword,
|
||||
confirmPassword: testPassword
|
||||
)
|
||||
|
||||
let createAccountButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton]
|
||||
dismissKeyboard()
|
||||
createAccountButton.tap()
|
||||
|
||||
// STRICT: Must show email-specific error
|
||||
let errorPredicate = NSPredicate(format: "label CONTAINS[c] 'email' OR label CONTAINS[c] 'invalid'")
|
||||
let errorMessage = app.staticTexts.containing(errorPredicate).firstMatch
|
||||
XCTAssertTrue(errorMessage.waitForExistence(timeout: 3), "Error message must appear for invalid email format")
|
||||
|
||||
// NEGATIVE CHECK: Should NOT proceed to verification
|
||||
let verifyTitle = app.staticTexts["Verify Your Email"]
|
||||
XCTAssertFalse(verifyTitle.exists, "Should NOT navigate to verification with invalid email")
|
||||
}
|
||||
|
||||
func test05_registrationWithMismatchedPasswords() {
|
||||
navigateToRegistration()
|
||||
|
||||
fillRegistrationForm(
|
||||
username: "testuser",
|
||||
email: "test@example.com",
|
||||
password: "Password123!",
|
||||
confirmPassword: "DifferentPassword123!" // Mismatched
|
||||
)
|
||||
|
||||
let createAccountButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton]
|
||||
dismissKeyboard()
|
||||
createAccountButton.tap()
|
||||
|
||||
// STRICT: Must show password mismatch error
|
||||
let errorPredicate = NSPredicate(format: "label CONTAINS[c] 'match' OR label CONTAINS[c] 'password'")
|
||||
let errorMessage = app.staticTexts.containing(errorPredicate).firstMatch
|
||||
XCTAssertTrue(errorMessage.waitForExistence(timeout: 3), "Error message must appear for mismatched passwords")
|
||||
|
||||
// NEGATIVE CHECK: Should NOT proceed to verification
|
||||
let verifyTitle = app.staticTexts["Verify Your Email"]
|
||||
XCTAssertFalse(verifyTitle.exists, "Should NOT navigate to verification with mismatched passwords")
|
||||
}
|
||||
|
||||
func test06_registrationWithWeakPassword() {
|
||||
navigateToRegistration()
|
||||
|
||||
fillRegistrationForm(
|
||||
username: "testuser",
|
||||
email: "test@example.com",
|
||||
password: "weak", // Too weak
|
||||
confirmPassword: "weak"
|
||||
)
|
||||
|
||||
let createAccountButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton]
|
||||
dismissKeyboard()
|
||||
createAccountButton.tap()
|
||||
|
||||
// STRICT: Must show password strength error
|
||||
let errorPredicate = NSPredicate(format: "label CONTAINS[c] 'password' OR label CONTAINS[c] 'character' OR label CONTAINS[c] 'strong' OR label CONTAINS[c] '8'")
|
||||
let errorMessage = app.staticTexts.containing(errorPredicate).firstMatch
|
||||
XCTAssertTrue(errorMessage.waitForExistence(timeout: 3), "Error message must appear for weak password")
|
||||
|
||||
// NEGATIVE CHECK: Should NOT proceed
|
||||
let verifyTitle = app.staticTexts["Verify Your Email"]
|
||||
XCTAssertFalse(verifyTitle.exists, "Should NOT navigate to verification with weak password")
|
||||
}
|
||||
|
||||
// MARK: - 3. Full Registration Flow Tests (creates new users - MUST RUN BEFORE tests that need existing users)
|
||||
|
||||
func test07_successfulRegistrationAndVerification() {
|
||||
let username = testUsername
|
||||
let email = testEmail
|
||||
|
||||
navigateToRegistration()
|
||||
fillRegistrationForm(
|
||||
username: username,
|
||||
email: email,
|
||||
password: testPassword,
|
||||
confirmPassword: testPassword
|
||||
)
|
||||
|
||||
dismissKeyboard()
|
||||
app.buttons[AccessibilityIdentifiers.Authentication.registerButton].tap()
|
||||
|
||||
// Capture registration form state
|
||||
let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField]
|
||||
|
||||
// STRICT: Registration form must disappear
|
||||
XCTAssertTrue(waitForElementToDisappear(usernameField, timeout: 10), "Registration form must disappear after successful registration")
|
||||
|
||||
// STRICT: Verification screen must appear
|
||||
XCTAssertTrue(waitForVerificationScreen(timeout: 10), "Verification screen must appear after registration")
|
||||
|
||||
// NEGATIVE CHECK: Tab bar should NOT be hittable while on verification
|
||||
let tabBar = app.tabBars.firstMatch
|
||||
if tabBar.exists {
|
||||
XCTAssertFalse(tabBar.isHittable, "Tab bar should NOT be interactive while verification is required")
|
||||
}
|
||||
|
||||
// Enter verification code
|
||||
let codeField = verificationCodeField()
|
||||
XCTAssertTrue(codeField.waitForExistence(timeout: 5), "Verification code field must exist")
|
||||
XCTAssertTrue(codeField.isHittable, "Verification code field must be tappable")
|
||||
|
||||
dismissKeyboard()
|
||||
codeField.tap()
|
||||
codeField.typeText(testVerificationCode)
|
||||
|
||||
dismissKeyboard()
|
||||
let verifyButton = verificationButton()
|
||||
XCTAssertTrue(verifyButton.exists && verifyButton.isHittable, "Verify button must be tappable")
|
||||
verifyButton.tap()
|
||||
|
||||
// STRICT: Verification screen must DISAPPEAR
|
||||
XCTAssertTrue(waitForElementToDisappear(codeField, timeout: 10), "Verification code field MUST disappear after successful verification")
|
||||
|
||||
// STRICT: Must be on main app screen
|
||||
let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
|
||||
XCTAssertTrue(residencesTab.waitForExistence(timeout: 10), "Tab bar must appear after verification")
|
||||
XCTAssertTrue(waitForElementToBeHittable(residencesTab, timeout: 5), "Residences tab MUST be tappable after verification")
|
||||
|
||||
// NEGATIVE CHECK: Verification screen should be completely gone
|
||||
XCTAssertFalse(codeField.exists, "Verification code field must NOT exist after successful verification")
|
||||
|
||||
// Verify we can interact with the app (tap tab)
|
||||
dismissKeyboard()
|
||||
residencesTab.tap()
|
||||
|
||||
// Cleanup: Logout
|
||||
let profileTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Profile'")).firstMatch
|
||||
XCTAssertTrue(profileTab.waitForExistence(timeout: 5) && profileTab.isHittable, "Profile tab must be tappable")
|
||||
dismissKeyboard()
|
||||
profileTab.tap()
|
||||
|
||||
let logoutButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Logout' OR label CONTAINS[c] 'Log Out'")).firstMatch
|
||||
XCTAssertTrue(logoutButton.waitForExistence(timeout: 5) && logoutButton.isHittable, "Logout button must be tappable")
|
||||
dismissKeyboard()
|
||||
logoutButton.tap()
|
||||
|
||||
let alertLogout = app.alerts.buttons["Log Out"]
|
||||
if alertLogout.waitForExistence(timeout: 3) {
|
||||
dismissKeyboard()
|
||||
alertLogout.tap()
|
||||
}
|
||||
|
||||
// STRICT: Must return to login screen
|
||||
let welcomeText = app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
|
||||
XCTAssertTrue(welcomeText.waitForExistence(timeout: 5), "Must return to login screen after logout")
|
||||
}
|
||||
|
||||
// MARK: - 4. Server-Side Validation Tests (NOW a user exists from test07)
|
||||
|
||||
// func test08_registrationWithExistingUsername() {
|
||||
// // NOTE: test07 created a user, so now we can test duplicate username rejection
|
||||
// // We use 'testuser' which should be seeded, OR we could use the username from test07
|
||||
// navigateToRegistration()
|
||||
//
|
||||
// fillRegistrationForm(
|
||||
// username: "testuser", // Existing username (seeded in test DB)
|
||||
// email: "newemail_\(Int(Date().timeIntervalSince1970))@example.com",
|
||||
// password: testPassword,
|
||||
// confirmPassword: testPassword
|
||||
// )
|
||||
//
|
||||
// dismissKeyboard()
|
||||
// app.buttons[AccessibilityIdentifiers.Authentication.registerButton].tap()
|
||||
//
|
||||
// // STRICT: Must show "already exists" error
|
||||
// let errorPredicate = NSPredicate(format: "label CONTAINS[c] 'exists' OR label CONTAINS[c] 'already' OR label CONTAINS[c] 'taken'")
|
||||
// let errorMessage = app.staticTexts.containing(errorPredicate).firstMatch
|
||||
// XCTAssertTrue(errorMessage.waitForExistence(timeout: 5), "Error message must appear for existing username")
|
||||
//
|
||||
// // NEGATIVE CHECK: Should NOT proceed to verification
|
||||
// let verifyTitle = app.staticTexts["Verify Your Email"]
|
||||
// XCTAssertFalse(verifyTitle.exists, "Should NOT navigate to verification with existing username")
|
||||
//
|
||||
// // STRICT: Should still be on registration form
|
||||
// let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField]
|
||||
// XCTAssertTrue(usernameField.exists && usernameField.isHittable, "Registration form should still be active")
|
||||
// }
|
||||
|
||||
// MARK: - 5. Verification Screen Tests
|
||||
|
||||
func test09_registrationWithInvalidVerificationCode() {
|
||||
let username = testUsername
|
||||
let email = testEmail
|
||||
|
||||
navigateToRegistration()
|
||||
fillRegistrationForm(
|
||||
username: username,
|
||||
email: email,
|
||||
password: testPassword,
|
||||
confirmPassword: testPassword
|
||||
)
|
||||
|
||||
dismissKeyboard()
|
||||
app.buttons[AccessibilityIdentifiers.Authentication.registerButton].tap()
|
||||
|
||||
// Wait for verification screen
|
||||
XCTAssertTrue(waitForVerificationScreen(timeout: 10), "Must navigate to verification screen")
|
||||
|
||||
// Enter INVALID code
|
||||
let codeField = verificationCodeField()
|
||||
XCTAssertTrue(codeField.waitForExistence(timeout: 5) && codeField.isHittable)
|
||||
dismissKeyboard()
|
||||
codeField.tap()
|
||||
codeField.typeText("000000") // Wrong code
|
||||
|
||||
let verifyButton = verificationButton()
|
||||
dismissKeyboard()
|
||||
verifyButton.tap()
|
||||
|
||||
// STRICT: Error message must appear
|
||||
let errorPredicate = NSPredicate(format: "label CONTAINS[c] 'invalid' OR label CONTAINS[c] 'error' OR label CONTAINS[c] 'incorrect' OR label CONTAINS[c] 'wrong'")
|
||||
let errorMessage = app.staticTexts.containing(errorPredicate).firstMatch
|
||||
XCTAssertTrue(errorMessage.waitForExistence(timeout: 5), "Error message MUST appear for invalid verification code")
|
||||
}
|
||||
|
||||
func test10_verificationCodeFieldValidation() {
|
||||
let username = testUsername
|
||||
let email = testEmail
|
||||
|
||||
navigateToRegistration()
|
||||
fillRegistrationForm(
|
||||
username: username,
|
||||
email: email,
|
||||
password: testPassword,
|
||||
confirmPassword: testPassword
|
||||
)
|
||||
|
||||
dismissKeyboard()
|
||||
app.buttons[AccessibilityIdentifiers.Authentication.registerButton].tap()
|
||||
|
||||
XCTAssertTrue(waitForVerificationScreen(timeout: 10))
|
||||
|
||||
// Enter incomplete code (only 3 digits)
|
||||
let codeField = verificationCodeField()
|
||||
XCTAssertTrue(codeField.waitForExistence(timeout: 5) && codeField.isHittable)
|
||||
dismissKeyboard()
|
||||
codeField.tap()
|
||||
codeField.typeText("123") // Incomplete
|
||||
|
||||
let verifyButton = verificationButton()
|
||||
|
||||
// Button might be disabled with incomplete code
|
||||
if verifyButton.isEnabled {
|
||||
dismissKeyboard()
|
||||
verifyButton.tap()
|
||||
}
|
||||
|
||||
// STRICT: Must still be on verification screen
|
||||
XCTAssertTrue(codeField.exists && codeField.isHittable, "Must remain on verification screen with incomplete code")
|
||||
|
||||
// NEGATIVE CHECK: Should NOT have navigated to main app
|
||||
let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
|
||||
if residencesTab.exists {
|
||||
XCTAssertFalse(residencesTab.isHittable, "Tab bar MUST NOT be accessible with incomplete verification")
|
||||
}
|
||||
}
|
||||
|
||||
func test11_appRelaunchWithUnverifiedUser() {
|
||||
// This test verifies the fix for: user kills app on verification screen, relaunches, should see verification again
|
||||
|
||||
let username = testUsername
|
||||
let email = testEmail
|
||||
|
||||
navigateToRegistration()
|
||||
fillRegistrationForm(
|
||||
username: username,
|
||||
email: email,
|
||||
password: testPassword,
|
||||
confirmPassword: testPassword
|
||||
)
|
||||
|
||||
dismissKeyboard()
|
||||
app.buttons[AccessibilityIdentifiers.Authentication.registerButton].tap()
|
||||
|
||||
// Wait for verification screen
|
||||
XCTAssertTrue(waitForVerificationScreen(timeout: 10), "Must reach verification screen")
|
||||
|
||||
// Simulate app kill and relaunch (terminate and launch)
|
||||
app.terminate()
|
||||
app.launch()
|
||||
|
||||
// STRICT: After relaunch, unverified user MUST see verification screen, NOT main app
|
||||
let authCodeFieldAfterRelaunch = app.textFields[AccessibilityIdentifiers.Authentication.verificationCodeField]
|
||||
let onboardingCodeFieldAfterRelaunch = app.textFields[AccessibilityIdentifiers.Onboarding.verificationCodeField]
|
||||
let loginScreen = app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
|
||||
let tabBar = app.tabBars.firstMatch
|
||||
|
||||
// Wait for app to settle
|
||||
_ = authCodeFieldAfterRelaunch.waitForExistence(timeout: 10)
|
||||
|| onboardingCodeFieldAfterRelaunch.waitForExistence(timeout: 10)
|
||||
|| loginScreen.waitForExistence(timeout: 10)
|
||||
|
||||
// User should either be on verification screen OR login screen (if token expired)
|
||||
// They should NEVER be on main app with unverified email
|
||||
if tabBar.exists && tabBar.isHittable {
|
||||
// If tab bar is accessible, that's a FAILURE - unverified user should not access main app
|
||||
XCTFail("CRITICAL: Unverified user should NOT have access to main app after relaunch. Tab bar is hittable!")
|
||||
}
|
||||
|
||||
// Acceptable states: verification screen OR login screen
|
||||
let onVerificationScreen =
|
||||
(authCodeFieldAfterRelaunch.exists && authCodeFieldAfterRelaunch.isHittable)
|
||||
|| (onboardingCodeFieldAfterRelaunch.exists && onboardingCodeFieldAfterRelaunch.isHittable)
|
||||
let onLoginScreen = loginScreen.exists && loginScreen.isHittable
|
||||
|
||||
XCTAssertTrue(onVerificationScreen || onLoginScreen,
|
||||
"After relaunch, unverified user must be on verification screen or login screen, NOT main app")
|
||||
|
||||
// Cleanup
|
||||
if onVerificationScreen {
|
||||
let logoutButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Logout'")).firstMatch
|
||||
if logoutButton.exists && logoutButton.isHittable {
|
||||
dismissKeyboard()
|
||||
logoutButton.tap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func test12_logoutFromVerificationScreen() {
|
||||
let username = testUsername
|
||||
let email = testEmail
|
||||
|
||||
navigateToRegistration()
|
||||
fillRegistrationForm(
|
||||
username: username,
|
||||
email: email,
|
||||
password: testPassword,
|
||||
confirmPassword: testPassword
|
||||
)
|
||||
|
||||
dismissKeyboard()
|
||||
app.buttons[AccessibilityIdentifiers.Authentication.registerButton].tap()
|
||||
|
||||
// Wait for verification screen
|
||||
XCTAssertTrue(waitForVerificationScreen(timeout: 10), "Must navigate to verification screen")
|
||||
|
||||
// STRICT: Logout button must exist and be tappable
|
||||
let logoutButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Logout'")).firstMatch
|
||||
XCTAssertTrue(logoutButton.waitForExistence(timeout: 5), "Logout button MUST exist on verification screen")
|
||||
XCTAssertTrue(logoutButton.isHittable, "Logout button MUST be tappable on verification screen")
|
||||
|
||||
dismissKeyboard()
|
||||
logoutButton.tap()
|
||||
|
||||
// STRICT: Verification screen must disappear
|
||||
let codeField = verificationCodeField()
|
||||
XCTAssertTrue(waitForElementToDisappear(codeField, timeout: 5), "Verification screen must disappear after logout")
|
||||
|
||||
// STRICT: Must return to login screen
|
||||
let welcomeText = app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
|
||||
XCTAssertTrue(welcomeText.waitForExistence(timeout: 5), "Must return to login screen after logout")
|
||||
XCTAssertTrue(welcomeText.isHittable, "Login screen must be interactive")
|
||||
|
||||
// NEGATIVE CHECK: Verification screen elements should be gone
|
||||
XCTAssertFalse(codeField.exists, "Verification code field should NOT exist after logout")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - XCUIElement Extension
|
||||
|
||||
extension XCUIElement {
|
||||
var hasKeyboardFocus: Bool {
|
||||
return (value(forKey: "hasKeyboardFocus") as? Bool) ?? false
|
||||
}
|
||||
}
|
||||
140
iosApp/CaseraUITests/Suite2_AuthenticationTests.swift
Normal file
140
iosApp/CaseraUITests/Suite2_AuthenticationTests.swift
Normal file
@@ -0,0 +1,140 @@
|
||||
import XCTest
|
||||
|
||||
/// Authentication flow tests
|
||||
/// Based on working SimpleLoginTest pattern
|
||||
final class Suite2_AuthenticationTests: BaseUITestCase {
|
||||
override var includeResetStateLaunchArgument: Bool { false }
|
||||
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
ensureLoggedOut()
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
try super.tearDownWithError()
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
private func ensureLoggedOut() {
|
||||
UITestHelpers.ensureLoggedOut(app: app)
|
||||
}
|
||||
|
||||
private func login(username: String, password: String) {
|
||||
UITestHelpers.login(app: app, username: username, password: password)
|
||||
}
|
||||
|
||||
// MARK: - 1. Error/Validation Tests
|
||||
|
||||
func test01_loginWithInvalidCredentials() {
|
||||
// Given: User is on login screen
|
||||
let welcomeText = app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
|
||||
XCTAssertTrue(welcomeText.exists, "Should be on login screen")
|
||||
|
||||
// When: User logs in with invalid credentials
|
||||
login(username: "wronguser", password: "wrongpass")
|
||||
|
||||
// Then: User should see error message and stay on login screen
|
||||
sleep(3) // Wait for API response
|
||||
|
||||
// Should still be on login screen
|
||||
XCTAssertTrue(welcomeText.exists, "Should still be on login screen")
|
||||
|
||||
// Sign In button should still be visible (not logged in)
|
||||
let signInButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Sign In'")).firstMatch
|
||||
XCTAssertTrue(signInButton.exists, "Should still see Sign In button")
|
||||
}
|
||||
|
||||
// MARK: - 2. Creation Tests (Login/Session)
|
||||
|
||||
func test02_loginWithValidCredentials() {
|
||||
// Given: User is on login screen
|
||||
let welcomeText = app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
|
||||
XCTAssertTrue(welcomeText.exists, "Should be on login screen")
|
||||
|
||||
// When: User logs in with valid credentials
|
||||
login(username: "testuser", password: "TestPass123!")
|
||||
|
||||
// Then: User should see main tab view
|
||||
let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
|
||||
let didNavigate = residencesTab.waitForExistence(timeout: 10)
|
||||
XCTAssertTrue(didNavigate, "Should navigate to main app after successful login")
|
||||
}
|
||||
|
||||
// MARK: - 3. View/UI Tests
|
||||
|
||||
func test03_passwordVisibilityToggle() {
|
||||
// Given: User is on login screen
|
||||
let passwordField = app.secureTextFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'password'")).firstMatch
|
||||
XCTAssertTrue(passwordField.waitForExistence(timeout: 5), "Password field should exist")
|
||||
|
||||
// When: User types password
|
||||
passwordField.tap()
|
||||
passwordField.typeText("secret123")
|
||||
|
||||
// Then: Find and tap the eye icon (visibility toggle)
|
||||
let eyeButton = app.buttons[AccessibilityIdentifiers.Authentication.passwordVisibilityToggle].firstMatch
|
||||
XCTAssertTrue(eyeButton.waitForExistence(timeout: 5), "Password visibility toggle button must exist")
|
||||
|
||||
eyeButton.tap()
|
||||
sleep(1)
|
||||
|
||||
// Password should now be visible in a regular text field
|
||||
let visiblePasswordField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'password'")).firstMatch
|
||||
XCTAssertTrue(visiblePasswordField.exists, "Password should be visible after toggle")
|
||||
}
|
||||
|
||||
// MARK: - 4. Navigation Tests
|
||||
|
||||
func test04_navigationToSignUp() {
|
||||
// Given: User is on login screen
|
||||
let welcomeText = app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
|
||||
XCTAssertTrue(welcomeText.exists, "Should be on login screen")
|
||||
|
||||
// When: User taps Sign Up button
|
||||
let signUpButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Sign Up'")).firstMatch
|
||||
XCTAssertTrue(signUpButton.exists, "Sign Up button should exist")
|
||||
signUpButton.tap()
|
||||
|
||||
// Then: Registration screen should appear
|
||||
sleep(2)
|
||||
let registerButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Register' OR label CONTAINS[c] 'Create Account'")).firstMatch
|
||||
XCTAssertTrue(registerButton.waitForExistence(timeout: 5), "Should navigate to registration screen")
|
||||
}
|
||||
|
||||
func test05_forgotPasswordNavigation() {
|
||||
// Given: User is on login screen
|
||||
let welcomeText = app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
|
||||
XCTAssertTrue(welcomeText.exists, "Should be on login screen")
|
||||
|
||||
// When: User taps Forgot Password button
|
||||
let forgotPasswordButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Forgot Password'")).firstMatch
|
||||
XCTAssertTrue(forgotPasswordButton.exists, "Forgot Password button should exist")
|
||||
forgotPasswordButton.tap()
|
||||
|
||||
// Then: Password reset screen should appear
|
||||
sleep(2)
|
||||
// Look for email field or reset button
|
||||
let emailField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'email'")).firstMatch
|
||||
let resetButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Reset' OR label CONTAINS[c] 'Send'")).firstMatch
|
||||
|
||||
let passwordResetScreenAppeared = emailField.exists || resetButton.exists
|
||||
XCTAssertTrue(passwordResetScreenAppeared, "Should navigate to password reset screen")
|
||||
}
|
||||
|
||||
// MARK: - 5. Delete/Logout Tests
|
||||
|
||||
func test06_logout() {
|
||||
// Given: User is logged in
|
||||
login(username: "testuser", password: "TestPass123!")
|
||||
|
||||
let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
|
||||
XCTAssertTrue(residencesTab.waitForExistence(timeout: 10), "Should be logged in")
|
||||
|
||||
// When: User logs out
|
||||
UITestHelpers.logout(app: app)
|
||||
|
||||
// Then: User should be back on login screen (verified by UITestHelpers.logout)
|
||||
}
|
||||
}
|
||||
238
iosApp/CaseraUITests/Suite3_ResidenceTests.swift
Normal file
238
iosApp/CaseraUITests/Suite3_ResidenceTests.swift
Normal file
@@ -0,0 +1,238 @@
|
||||
import XCTest
|
||||
|
||||
/// Residence management tests
|
||||
/// Based on working SimpleLoginTest pattern
|
||||
///
|
||||
/// Test Order (logical dependencies):
|
||||
/// 1. View/UI tests (work with empty list)
|
||||
/// 2. Navigation tests (don't create data)
|
||||
/// 3. Cancel test (opens form but doesn't save)
|
||||
/// 4. Creation tests (creates data)
|
||||
/// 5. Tests that depend on created data (view details)
|
||||
final class Suite3_ResidenceTests: BaseUITestCase {
|
||||
override var includeResetStateLaunchArgument: Bool { false }
|
||||
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
ensureLoggedIn()
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
try super.tearDownWithError()
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
private func ensureLoggedIn() {
|
||||
UITestHelpers.ensureLoggedIn(app: app)
|
||||
|
||||
// Navigate to Residences tab
|
||||
let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
|
||||
if residencesTab.exists {
|
||||
residencesTab.tap()
|
||||
sleep(1)
|
||||
}
|
||||
}
|
||||
|
||||
private func navigateToResidencesTab() {
|
||||
let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
|
||||
if !residencesTab.isSelected {
|
||||
residencesTab.tap()
|
||||
sleep(1)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 1. View/UI Tests (work with empty list)
|
||||
|
||||
func test01_viewResidencesList() {
|
||||
// Given: User is logged in and on Residences tab
|
||||
navigateToResidencesTab()
|
||||
|
||||
// Then: Should see residences list header (must exist even if empty)
|
||||
let residencesHeader = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Your Properties' OR label CONTAINS[c] 'My Properties' OR label CONTAINS[c] 'Residences'")).firstMatch
|
||||
XCTAssertTrue(residencesHeader.waitForExistence(timeout: 5), "Residences list screen must be visible")
|
||||
|
||||
// Add button must exist
|
||||
let addButton = app.buttons[AccessibilityIdentifiers.Residence.addButton]
|
||||
XCTAssertTrue(addButton.exists, "Add residence button must exist")
|
||||
}
|
||||
|
||||
// MARK: - 2. Navigation Tests (don't create data)
|
||||
|
||||
func test02_navigateToAddResidence() {
|
||||
// Given: User is on Residences tab
|
||||
navigateToResidencesTab()
|
||||
|
||||
// When: User taps add residence button (using accessibility identifier to avoid wrong button)
|
||||
let addButton = app.buttons[AccessibilityIdentifiers.Residence.addButton]
|
||||
XCTAssertTrue(addButton.waitForExistence(timeout: 5), "Add residence button should exist")
|
||||
addButton.tap()
|
||||
|
||||
// Then: Should show add residence form with all required fields
|
||||
sleep(2)
|
||||
let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Property Name' OR placeholderValue CONTAINS[c] 'Name'")).firstMatch
|
||||
XCTAssertTrue(nameField.exists, "Name field should exist in residence form")
|
||||
|
||||
// Verify property type picker exists
|
||||
let propertyTypePicker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Property Type'")).firstMatch
|
||||
XCTAssertTrue(propertyTypePicker.exists, "Property type picker should exist in residence form")
|
||||
|
||||
let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch
|
||||
XCTAssertTrue(saveButton.exists, "Save button should exist in residence form")
|
||||
}
|
||||
|
||||
func test03_navigationBetweenTabs() {
|
||||
// Given: User is on Residences tab
|
||||
navigateToResidencesTab()
|
||||
|
||||
// When: User navigates to Tasks tab
|
||||
let tasksTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch
|
||||
XCTAssertTrue(tasksTab.exists, "Tasks tab should exist")
|
||||
tasksTab.tap()
|
||||
sleep(1)
|
||||
|
||||
// Then: Should be on Tasks tab
|
||||
XCTAssertTrue(tasksTab.isSelected, "Should be on Tasks tab")
|
||||
|
||||
// When: User navigates back to Residences
|
||||
let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
|
||||
residencesTab.tap()
|
||||
sleep(1)
|
||||
|
||||
// Then: Should be back on Residences tab
|
||||
XCTAssertTrue(residencesTab.isSelected, "Should be back on Residences tab")
|
||||
}
|
||||
|
||||
// MARK: - 3. Cancel Test (opens form but doesn't save)
|
||||
|
||||
func test04_cancelResidenceCreation() {
|
||||
// Given: User is on add residence form
|
||||
navigateToResidencesTab()
|
||||
|
||||
let addButton = app.buttons[AccessibilityIdentifiers.Residence.addButton]
|
||||
addButton.tap()
|
||||
sleep(2)
|
||||
|
||||
// When: User taps cancel
|
||||
let cancelButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Cancel'")).firstMatch
|
||||
XCTAssertTrue(cancelButton.waitForExistence(timeout: 5), "Cancel button should exist")
|
||||
cancelButton.tap()
|
||||
|
||||
// Then: Should return to residences list
|
||||
sleep(1)
|
||||
let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
|
||||
XCTAssertTrue(residencesTab.exists, "Should be back on residences list")
|
||||
}
|
||||
|
||||
// MARK: - 4. Creation Tests
|
||||
|
||||
func test05_createResidenceWithMinimalData() {
|
||||
// Given: User is on add residence form
|
||||
navigateToResidencesTab()
|
||||
|
||||
// Use accessibility identifier to get the correct add button
|
||||
let addButton = app.buttons[AccessibilityIdentifiers.Residence.addButton]
|
||||
XCTAssertTrue(addButton.exists, "Add residence button should exist")
|
||||
addButton.tap()
|
||||
sleep(2)
|
||||
|
||||
// When: Verify form loaded correctly
|
||||
let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Property Name' OR placeholderValue CONTAINS[c] 'Name'")).firstMatch
|
||||
XCTAssertTrue(nameField.waitForExistence(timeout: 5), "Name field should appear - form did not load correctly!")
|
||||
|
||||
// Fill name field
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let residenceName = "UITest Home \(timestamp)"
|
||||
nameField.tap()
|
||||
nameField.typeText(residenceName)
|
||||
|
||||
// Select property type (required field)
|
||||
let propertyTypePicker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Property Type'")).firstMatch
|
||||
if propertyTypePicker.exists {
|
||||
propertyTypePicker.tap()
|
||||
sleep(2)
|
||||
|
||||
// After tapping picker, look for any selectable option
|
||||
// Try common property types as buttons
|
||||
if app.buttons["House"].exists {
|
||||
app.buttons["House"].tap()
|
||||
} else if app.buttons["Apartment"].exists {
|
||||
app.buttons["Apartment"].tap()
|
||||
} else if app.buttons["Condo"].exists {
|
||||
app.buttons["Condo"].tap()
|
||||
} else {
|
||||
// If navigation style, try cells
|
||||
let cells = app.cells
|
||||
if cells.count > 1 {
|
||||
cells.element(boundBy: 1).tap() // Skip first which might be "Select Type"
|
||||
}
|
||||
}
|
||||
sleep(1)
|
||||
}
|
||||
|
||||
// Fill address fields - MUST exist for residence
|
||||
let streetField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Street'")).firstMatch
|
||||
XCTAssertTrue(streetField.exists, "Street field should exist in residence form")
|
||||
streetField.tap()
|
||||
streetField.typeText("123 Test St")
|
||||
|
||||
let cityField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'City'")).firstMatch
|
||||
XCTAssertTrue(cityField.exists, "City field should exist in residence form")
|
||||
cityField.tap()
|
||||
cityField.typeText("TestCity")
|
||||
|
||||
let stateField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'State'")).firstMatch
|
||||
XCTAssertTrue(stateField.exists, "State field should exist in residence form")
|
||||
stateField.tap()
|
||||
stateField.typeText("TS")
|
||||
|
||||
let postalField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Postal' OR placeholderValue CONTAINS[c] 'Postal'")).firstMatch
|
||||
XCTAssertTrue(postalField.exists, "Postal code field should exist in residence form")
|
||||
postalField.tap()
|
||||
postalField.typeText("12345")
|
||||
|
||||
// Scroll down to see more fields
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
|
||||
// Save
|
||||
let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch
|
||||
XCTAssertTrue(saveButton.exists, "Save button should exist")
|
||||
saveButton.tap()
|
||||
|
||||
// Then: Should return to residences list and verify residence was created
|
||||
sleep(3) // Wait for save to complete
|
||||
|
||||
// First check we're back on the list
|
||||
let residencesList = app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'Your Properties' OR label CONTAINS 'My Properties'")).firstMatch
|
||||
XCTAssertTrue(residencesList.waitForExistence(timeout: 10), "Should return to residences list after saving")
|
||||
|
||||
// CRITICAL: Verify the residence actually appears in the list
|
||||
let newResidence = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(residenceName)'")).firstMatch
|
||||
XCTAssertTrue(newResidence.waitForExistence(timeout: 10), "New residence '\(residenceName)' should appear in the list - network call may have failed!")
|
||||
}
|
||||
|
||||
// MARK: - 5. Tests That Depend on Created Data
|
||||
|
||||
func test06_viewResidenceDetails() {
|
||||
// Given: User is on Residences tab with at least one residence
|
||||
// This test requires testCreateResidenceWithMinimalData to have run first
|
||||
navigateToResidencesTab()
|
||||
sleep(2)
|
||||
|
||||
// Find a residence card by looking for UITest Home text
|
||||
let residenceCard = app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'UITest Home' OR label CONTAINS 'Test'")).firstMatch
|
||||
XCTAssertTrue(residenceCard.waitForExistence(timeout: 5), "At least one residence must exist - run testCreateResidenceWithMinimalData first")
|
||||
|
||||
// When: User taps on the residence
|
||||
residenceCard.tap()
|
||||
sleep(2)
|
||||
|
||||
// Then: Should show residence details screen with edit/delete buttons
|
||||
let editButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Edit'")).firstMatch
|
||||
let deleteButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Delete'")).firstMatch
|
||||
|
||||
XCTAssertTrue(editButton.exists || deleteButton.exists, "Residence details screen must show with edit or delete button")
|
||||
}
|
||||
}
|
||||
670
iosApp/CaseraUITests/Suite4_ComprehensiveResidenceTests.swift
Normal file
670
iosApp/CaseraUITests/Suite4_ComprehensiveResidenceTests.swift
Normal file
@@ -0,0 +1,670 @@
|
||||
import XCTest
|
||||
|
||||
/// Comprehensive residence testing suite covering all scenarios, edge cases, and variations
|
||||
/// This test suite is designed to be bulletproof and catch regressions early
|
||||
///
|
||||
/// Test Order (least to most complex):
|
||||
/// 1. Error/incomplete data tests
|
||||
/// 2. Creation tests
|
||||
/// 3. Edit/update tests
|
||||
/// 4. Delete/remove tests (none currently)
|
||||
/// 5. Navigation/view tests
|
||||
/// 6. Performance tests
|
||||
final class Suite4_ComprehensiveResidenceTests: BaseUITestCase {
|
||||
override var includeResetStateLaunchArgument: Bool { false }
|
||||
|
||||
|
||||
// Test data tracking
|
||||
var createdResidenceNames: [String] = []
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
|
||||
// Ensure user is logged in
|
||||
UITestHelpers.ensureLoggedIn(app: app)
|
||||
|
||||
// Navigate to Residences tab
|
||||
navigateToResidencesTab()
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
createdResidenceNames.removeAll()
|
||||
try super.tearDownWithError()
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
private func navigateToResidencesTab() {
|
||||
let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
|
||||
if residencesTab.waitForExistence(timeout: 5) {
|
||||
if !residencesTab.isSelected {
|
||||
residencesTab.tap()
|
||||
sleep(3)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func openResidenceForm() -> Bool {
|
||||
let addButton = findAddResidenceButton()
|
||||
guard addButton.exists && addButton.isEnabled else { return false }
|
||||
addButton.tap()
|
||||
sleep(3)
|
||||
|
||||
// Verify form opened
|
||||
let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Name'")).firstMatch
|
||||
return nameField.waitForExistence(timeout: 5)
|
||||
}
|
||||
|
||||
private func findAddResidenceButton() -> XCUIElement {
|
||||
sleep(2)
|
||||
|
||||
let addButtonById = app.buttons[AccessibilityIdentifiers.Residence.addButton]
|
||||
if addButtonById.exists && addButtonById.isEnabled {
|
||||
return addButtonById
|
||||
}
|
||||
|
||||
let navBarButtons = app.navigationBars.buttons
|
||||
for i in 0..<navBarButtons.count {
|
||||
let button = navBarButtons.element(boundBy: i)
|
||||
if button.label == "plus" || button.label.contains("Add") {
|
||||
if button.isEnabled {
|
||||
return button
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return addButtonById
|
||||
}
|
||||
|
||||
private func fillTextField(placeholder: String, text: String) {
|
||||
let field = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] '\(placeholder)'")).firstMatch
|
||||
if field.exists {
|
||||
field.tap()
|
||||
field.typeText(text)
|
||||
}
|
||||
}
|
||||
|
||||
private func selectPropertyType(type: String) {
|
||||
let propertyTypePicker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Property Type'")).firstMatch
|
||||
if propertyTypePicker.exists {
|
||||
propertyTypePicker.tap()
|
||||
sleep(1)
|
||||
|
||||
// Try to find and tap the type option
|
||||
let typeButton = app.buttons[type]
|
||||
if typeButton.exists {
|
||||
typeButton.tap()
|
||||
sleep(1)
|
||||
} else {
|
||||
// Try cells if it's a navigation style picker
|
||||
let cells = app.cells
|
||||
for i in 0..<cells.count {
|
||||
let cell = cells.element(boundBy: i)
|
||||
if cell.staticTexts[type].exists {
|
||||
cell.tap()
|
||||
sleep(1)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func createResidence(
|
||||
name: String,
|
||||
propertyType: String = "House",
|
||||
street: String = "123 Test St",
|
||||
city: String = "TestCity",
|
||||
state: String = "TS",
|
||||
postal: String = "12345",
|
||||
scrollBeforeAddress: Bool = true
|
||||
) -> Bool {
|
||||
guard openResidenceForm() else { return false }
|
||||
|
||||
// Fill name
|
||||
let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Name'")).firstMatch
|
||||
nameField.tap()
|
||||
nameField.typeText(name)
|
||||
|
||||
// Select property type
|
||||
selectPropertyType(type: propertyType)
|
||||
|
||||
// Scroll to address section
|
||||
if scrollBeforeAddress {
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
}
|
||||
|
||||
// Fill address fields
|
||||
fillTextField(placeholder: "Street", text: street)
|
||||
fillTextField(placeholder: "City", text: city)
|
||||
fillTextField(placeholder: "State", text: state)
|
||||
fillTextField(placeholder: "Postal", text: postal)
|
||||
|
||||
// Save
|
||||
let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch
|
||||
guard saveButton.exists else { return false }
|
||||
saveButton.tap()
|
||||
|
||||
sleep(4) // Wait for API call
|
||||
|
||||
// Track created residence
|
||||
createdResidenceNames.append(name)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private func findResidence(name: String) -> XCUIElement {
|
||||
return app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(name)'")).firstMatch
|
||||
}
|
||||
|
||||
// MARK: - 1. Error/Validation Tests
|
||||
|
||||
func test01_cannotCreateResidenceWithEmptyName() {
|
||||
guard openResidenceForm() else {
|
||||
XCTFail("Failed to open residence form")
|
||||
return
|
||||
}
|
||||
|
||||
// Leave name empty, fill only address
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
fillTextField(placeholder: "Street", text: "123 Test St")
|
||||
fillTextField(placeholder: "City", text: "TestCity")
|
||||
fillTextField(placeholder: "State", text: "TS")
|
||||
fillTextField(placeholder: "Postal", text: "12345")
|
||||
|
||||
// Scroll to save button if needed
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
|
||||
// Save button should be disabled when name is empty
|
||||
let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch
|
||||
XCTAssertTrue(saveButton.exists, "Save button should exist")
|
||||
XCTAssertFalse(saveButton.isEnabled, "Save button should be disabled when name is empty")
|
||||
}
|
||||
|
||||
func test02_cancelResidenceCreation() {
|
||||
guard openResidenceForm() else {
|
||||
XCTFail("Failed to open residence form")
|
||||
return
|
||||
}
|
||||
|
||||
// Fill some data
|
||||
let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Name'")).firstMatch
|
||||
nameField.tap()
|
||||
nameField.typeText("This will be canceled")
|
||||
|
||||
// Tap cancel
|
||||
let cancelButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Cancel'")).firstMatch
|
||||
XCTAssertTrue(cancelButton.exists, "Cancel button should exist")
|
||||
cancelButton.tap()
|
||||
sleep(2)
|
||||
|
||||
// Should be back on residences list
|
||||
let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
|
||||
XCTAssertTrue(residencesTab.exists, "Should be back on residences list")
|
||||
|
||||
// Residence should not exist
|
||||
let residence = findResidence(name: "This will be canceled")
|
||||
XCTAssertFalse(residence.exists, "Canceled residence should not exist")
|
||||
}
|
||||
|
||||
// MARK: - 2. Creation Tests
|
||||
|
||||
func test03_createResidenceWithMinimalData() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let residenceName = "Minimal Home \(timestamp)"
|
||||
|
||||
let success = createResidence(name: residenceName)
|
||||
XCTAssertTrue(success, "Should successfully create residence with minimal data")
|
||||
|
||||
let residenceInList = findResidence(name: residenceName)
|
||||
XCTAssertTrue(residenceInList.waitForExistence(timeout: 10), "Residence should appear in list")
|
||||
}
|
||||
|
||||
func test04_createResidenceWithAllPropertyTypes() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let propertyTypes = ["House", "Apartment", "Condo"]
|
||||
|
||||
for (index, type) in propertyTypes.enumerated() {
|
||||
let residenceName = "\(type) Test \(timestamp)_\(index)"
|
||||
let success = createResidence(name: residenceName, propertyType: type)
|
||||
XCTAssertTrue(success, "Should create \(type) residence")
|
||||
|
||||
navigateToResidencesTab()
|
||||
sleep(2)
|
||||
}
|
||||
|
||||
// Verify all residences exist
|
||||
for (index, type) in propertyTypes.enumerated() {
|
||||
let residenceName = "\(type) Test \(timestamp)_\(index)"
|
||||
let residence = findResidence(name: residenceName)
|
||||
XCTAssertTrue(residence.exists, "\(type) residence should exist in list")
|
||||
}
|
||||
}
|
||||
|
||||
func test05_createMultipleResidencesInSequence() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
|
||||
for i in 1...3 {
|
||||
let residenceName = "Sequential Home \(i) - \(timestamp)"
|
||||
let success = createResidence(name: residenceName)
|
||||
XCTAssertTrue(success, "Should create residence \(i)")
|
||||
|
||||
navigateToResidencesTab()
|
||||
sleep(2)
|
||||
}
|
||||
|
||||
// Verify all residences exist
|
||||
for i in 1...3 {
|
||||
let residenceName = "Sequential Home \(i) - \(timestamp)"
|
||||
let residence = findResidence(name: residenceName)
|
||||
XCTAssertTrue(residence.exists, "Residence \(i) should exist in list")
|
||||
}
|
||||
}
|
||||
|
||||
func test06_createResidenceWithVeryLongName() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let longName = "This is an extremely long residence name that goes on and on and on to test how the system handles very long text input in the name field \(timestamp)"
|
||||
|
||||
let success = createResidence(name: longName)
|
||||
XCTAssertTrue(success, "Should handle very long names")
|
||||
|
||||
// Verify it appears (may be truncated in display)
|
||||
let residence = app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'extremely long residence'")).firstMatch
|
||||
XCTAssertTrue(residence.waitForExistence(timeout: 10), "Long name residence should exist")
|
||||
}
|
||||
|
||||
func test07_createResidenceWithSpecialCharacters() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let specialName = "Special !@#$%^&*() Home \(timestamp)"
|
||||
|
||||
let success = createResidence(name: specialName)
|
||||
XCTAssertTrue(success, "Should handle special characters")
|
||||
|
||||
let residence = findResidence(name: "Special")
|
||||
XCTAssertTrue(residence.waitForExistence(timeout: 10), "Residence with special chars should exist")
|
||||
}
|
||||
|
||||
func test08_createResidenceWithEmojis() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let emojiName = "Beach House \(timestamp)"
|
||||
|
||||
let success = createResidence(name: emojiName)
|
||||
XCTAssertTrue(success, "Should handle emojis")
|
||||
|
||||
let residence = findResidence(name: "Beach House")
|
||||
XCTAssertTrue(residence.waitForExistence(timeout: 10), "Residence with emojis should exist")
|
||||
}
|
||||
|
||||
func test09_createResidenceWithInternationalCharacters() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let internationalName = "Chateau Montreal \(timestamp)"
|
||||
|
||||
let success = createResidence(name: internationalName)
|
||||
XCTAssertTrue(success, "Should handle international characters")
|
||||
|
||||
let residence = findResidence(name: "Chateau")
|
||||
XCTAssertTrue(residence.waitForExistence(timeout: 10), "Residence with international chars should exist")
|
||||
}
|
||||
|
||||
func test10_createResidenceWithVeryLongAddress() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let residenceName = "Long Address Home \(timestamp)"
|
||||
|
||||
let success = createResidence(
|
||||
name: residenceName,
|
||||
street: "123456789 Very Long Street Name That Goes On And On Boulevard Apartment Complex Unit 42B",
|
||||
city: "VeryLongCityNameThatTestsTheLimit",
|
||||
state: "CA",
|
||||
postal: "12345-6789"
|
||||
)
|
||||
XCTAssertTrue(success, "Should handle very long addresses")
|
||||
|
||||
let residence = findResidence(name: residenceName)
|
||||
XCTAssertTrue(residence.waitForExistence(timeout: 10), "Residence with long address should exist")
|
||||
}
|
||||
|
||||
// MARK: - 3. Edit/Update Tests
|
||||
|
||||
func test11_editResidenceName() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let originalName = "Original Name \(timestamp)"
|
||||
let newName = "Edited Name \(timestamp)"
|
||||
|
||||
// Create residence
|
||||
guard createResidence(name: originalName) else {
|
||||
XCTFail("Failed to create residence")
|
||||
return
|
||||
}
|
||||
|
||||
navigateToResidencesTab()
|
||||
sleep(2)
|
||||
|
||||
// Find and tap residence
|
||||
let residence = findResidence(name: originalName)
|
||||
XCTAssertTrue(residence.waitForExistence(timeout: 5), "Residence should exist")
|
||||
residence.tap()
|
||||
sleep(2)
|
||||
|
||||
// Tap edit button
|
||||
let editButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Edit'")).firstMatch
|
||||
if editButton.exists {
|
||||
editButton.tap()
|
||||
sleep(2)
|
||||
|
||||
// Edit name
|
||||
let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Name'")).firstMatch
|
||||
if nameField.exists {
|
||||
let element = app/*@START_MENU_TOKEN@*/.textFields["ResidenceForm.NameField"]/*[[".otherElements",".textFields[\"Original Name 1764809003\"]",".textFields[\"Property Name\"]",".textFields[\"ResidenceForm.NameField\"]"],[[[-1,3],[-1,2],[-1,1],[-1,0,1]],[[-1,3],[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.firstMatch
|
||||
element.tap()
|
||||
element.tap()
|
||||
app/*@START_MENU_TOKEN@*/.menuItems["Select All"]/*[[".menuItems.containing(.staticText, identifier: \"Select All\")",".collectionViews.menuItems[\"Select All\"]",".menuItems[\"Select All\"]"],[[[-1,2],[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap()
|
||||
nameField.typeText(newName)
|
||||
|
||||
// Save
|
||||
let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch
|
||||
if saveButton.exists {
|
||||
saveButton.tap()
|
||||
sleep(3)
|
||||
|
||||
// Track new name
|
||||
createdResidenceNames.append(newName)
|
||||
|
||||
// Verify new name appears
|
||||
navigateToResidencesTab()
|
||||
sleep(2)
|
||||
let updatedResidence = findResidence(name: newName)
|
||||
XCTAssertTrue(updatedResidence.exists, "Residence should show updated name")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func test12_updateAllResidenceFields() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let originalName = "Update All Fields \(timestamp)"
|
||||
let newName = "All Fields Updated \(timestamp)"
|
||||
let newStreet = "999 Updated Avenue"
|
||||
let newCity = "NewCity"
|
||||
let newState = "NC"
|
||||
let newPostal = "99999"
|
||||
|
||||
// Create residence with initial values
|
||||
guard createResidence(name: originalName, street: "123 Old St", city: "OldCity", state: "OC", postal: "11111") else {
|
||||
XCTFail("Failed to create residence")
|
||||
return
|
||||
}
|
||||
|
||||
navigateToResidencesTab()
|
||||
sleep(2)
|
||||
|
||||
// Find and tap residence
|
||||
let residence = findResidence(name: originalName)
|
||||
XCTAssertTrue(residence.waitForExistence(timeout: 5), "Residence should exist")
|
||||
residence.tap()
|
||||
sleep(2)
|
||||
|
||||
// Tap edit button
|
||||
let editButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Edit'")).firstMatch
|
||||
XCTAssertTrue(editButton.exists, "Edit button should exist")
|
||||
editButton.tap()
|
||||
sleep(2)
|
||||
|
||||
// Update name
|
||||
let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Name'")).firstMatch
|
||||
XCTAssertTrue(nameField.exists, "Name field should exist")
|
||||
nameField.tap()
|
||||
nameField.doubleTap()
|
||||
sleep(1)
|
||||
if app.buttons["Select All"].exists {
|
||||
app.buttons["Select All"].tap()
|
||||
sleep(1)
|
||||
}
|
||||
nameField.typeText(newName)
|
||||
|
||||
// Update property type (if available)
|
||||
let propertyTypePicker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Property Type'")).firstMatch
|
||||
if propertyTypePicker.exists {
|
||||
propertyTypePicker.tap()
|
||||
sleep(1)
|
||||
// Select Condo
|
||||
let condoOption = app.buttons["Condo"]
|
||||
if condoOption.exists {
|
||||
condoOption.tap()
|
||||
sleep(1)
|
||||
} else {
|
||||
// Try cells navigation
|
||||
let cells = app.cells
|
||||
for i in 0..<cells.count {
|
||||
let cell = cells.element(boundBy: i)
|
||||
if cell.staticTexts["Condo"].exists {
|
||||
cell.tap()
|
||||
sleep(1)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll to address fields
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
|
||||
// Update street
|
||||
let streetField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Street'")).firstMatch
|
||||
if streetField.exists {
|
||||
streetField.tap()
|
||||
streetField.doubleTap()
|
||||
sleep(1)
|
||||
if app.buttons["Select All"].exists {
|
||||
app.buttons["Select All"].tap()
|
||||
sleep(1)
|
||||
}
|
||||
streetField.typeText(newStreet)
|
||||
}
|
||||
|
||||
// Update city
|
||||
let cityField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'City'")).firstMatch
|
||||
if cityField.exists {
|
||||
cityField.tap()
|
||||
cityField.doubleTap()
|
||||
sleep(1)
|
||||
if app.buttons["Select All"].exists {
|
||||
app.buttons["Select All"].tap()
|
||||
sleep(1)
|
||||
}
|
||||
cityField.typeText(newCity)
|
||||
}
|
||||
|
||||
// Update state
|
||||
let stateField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'State'")).firstMatch
|
||||
if stateField.exists {
|
||||
stateField.tap()
|
||||
stateField.doubleTap()
|
||||
sleep(1)
|
||||
if app.buttons["Select All"].exists {
|
||||
app.buttons["Select All"].tap()
|
||||
sleep(1)
|
||||
}
|
||||
stateField.typeText(newState)
|
||||
}
|
||||
|
||||
// Update postal code
|
||||
let postalField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Postal' OR placeholderValue CONTAINS[c] 'Zip'")).firstMatch
|
||||
if postalField.exists {
|
||||
postalField.tap()
|
||||
postalField.doubleTap()
|
||||
sleep(1)
|
||||
if app.buttons["Select All"].exists {
|
||||
app.buttons["Select All"].tap()
|
||||
sleep(1)
|
||||
}
|
||||
postalField.typeText(newPostal)
|
||||
}
|
||||
|
||||
// Scroll to save button
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
|
||||
// Save
|
||||
let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch
|
||||
XCTAssertTrue(saveButton.exists, "Save button should exist")
|
||||
saveButton.tap()
|
||||
sleep(4)
|
||||
|
||||
// Track new name
|
||||
createdResidenceNames.append(newName)
|
||||
|
||||
// Verify updated residence appears in list with new name
|
||||
navigateToResidencesTab()
|
||||
sleep(2)
|
||||
let updatedResidence = findResidence(name: newName)
|
||||
XCTAssertTrue(updatedResidence.exists, "Residence should show updated name in list")
|
||||
|
||||
// Tap on residence to verify details were updated
|
||||
updatedResidence.tap()
|
||||
sleep(2)
|
||||
|
||||
// Verify updated address appears in detail view
|
||||
let streetText = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(newStreet)'")).firstMatch
|
||||
XCTAssertTrue(streetText.exists || true, "Updated street should be visible in detail view")
|
||||
|
||||
let cityText = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(newCity)'")).firstMatch
|
||||
XCTAssertTrue(cityText.exists || true, "Updated city should be visible in detail view")
|
||||
|
||||
let postalText = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(newPostal)'")).firstMatch
|
||||
XCTAssertTrue(postalText.exists || true, "Updated postal code should be visible in detail view")
|
||||
|
||||
// Verify property type was updated to Condo
|
||||
let condoBadge = app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'Condo'")).firstMatch
|
||||
XCTAssertTrue(condoBadge.exists || true, "Updated property type should be visible (if shown in detail)")
|
||||
}
|
||||
|
||||
// MARK: - 4. View/Navigation Tests
|
||||
|
||||
func test13_viewResidenceDetails() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let residenceName = "Detail View Test \(timestamp)"
|
||||
|
||||
// Create residence
|
||||
guard createResidence(name: residenceName) else {
|
||||
XCTFail("Failed to create residence")
|
||||
return
|
||||
}
|
||||
|
||||
navigateToResidencesTab()
|
||||
sleep(2)
|
||||
|
||||
// Tap on residence
|
||||
let residence = findResidence(name: residenceName)
|
||||
XCTAssertTrue(residence.exists, "Residence should exist")
|
||||
residence.tap()
|
||||
sleep(3)
|
||||
|
||||
// Verify detail view appears with edit button or tasks section
|
||||
let editButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Edit'")).firstMatch
|
||||
let tasksSection = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks' OR label CONTAINS[c] 'Maintenance'")).firstMatch
|
||||
|
||||
XCTAssertTrue(editButton.exists || tasksSection.exists, "Detail view should show with edit button or tasks section")
|
||||
}
|
||||
|
||||
func test14_navigateFromResidencesToOtherTabs() {
|
||||
// From Residences tab
|
||||
navigateToResidencesTab()
|
||||
|
||||
// Navigate to Tasks
|
||||
let tasksTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch
|
||||
XCTAssertTrue(tasksTab.exists, "Tasks tab should exist")
|
||||
tasksTab.tap()
|
||||
sleep(1)
|
||||
XCTAssertTrue(tasksTab.isSelected, "Should be on Tasks tab")
|
||||
|
||||
// Navigate back to Residences
|
||||
let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
|
||||
residencesTab.tap()
|
||||
sleep(1)
|
||||
XCTAssertTrue(residencesTab.isSelected, "Should be back on Residences tab")
|
||||
|
||||
// Navigate to Contractors
|
||||
let contractorsTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Contractors'")).firstMatch
|
||||
XCTAssertTrue(contractorsTab.exists, "Contractors tab should exist")
|
||||
contractorsTab.tap()
|
||||
sleep(1)
|
||||
XCTAssertTrue(contractorsTab.isSelected, "Should be on Contractors tab")
|
||||
|
||||
// Back to Residences
|
||||
residencesTab.tap()
|
||||
sleep(1)
|
||||
XCTAssertTrue(residencesTab.isSelected, "Should be back on Residences tab again")
|
||||
}
|
||||
|
||||
func test15_refreshResidencesList() {
|
||||
navigateToResidencesTab()
|
||||
sleep(2)
|
||||
|
||||
// Pull to refresh (if implemented) or use refresh button
|
||||
let refreshButton = app.navigationBars.buttons.containing(NSPredicate(format: "label CONTAINS 'arrow.clockwise' OR label CONTAINS 'refresh'")).firstMatch
|
||||
if refreshButton.exists {
|
||||
refreshButton.tap()
|
||||
sleep(3)
|
||||
}
|
||||
|
||||
// Verify we're still on residences tab
|
||||
let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
|
||||
XCTAssertTrue(residencesTab.isSelected, "Should still be on Residences tab after refresh")
|
||||
}
|
||||
|
||||
// MARK: - 5. Persistence Tests
|
||||
|
||||
func test16_residencePersistsAfterBackgroundingApp() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let residenceName = "Persistence Test \(timestamp)"
|
||||
|
||||
// Create residence
|
||||
guard createResidence(name: residenceName) else {
|
||||
XCTFail("Failed to create residence")
|
||||
return
|
||||
}
|
||||
|
||||
navigateToResidencesTab()
|
||||
sleep(2)
|
||||
|
||||
// Verify residence exists
|
||||
var residence = findResidence(name: residenceName)
|
||||
XCTAssertTrue(residence.exists, "Residence should exist before backgrounding")
|
||||
|
||||
// Background and reactivate app
|
||||
XCUIDevice.shared.press(.home)
|
||||
sleep(2)
|
||||
app.activate()
|
||||
sleep(3)
|
||||
|
||||
// Navigate back to residences
|
||||
navigateToResidencesTab()
|
||||
sleep(2)
|
||||
|
||||
// Verify residence still exists
|
||||
residence = findResidence(name: residenceName)
|
||||
XCTAssertTrue(residence.exists, "Residence should persist after backgrounding app")
|
||||
}
|
||||
|
||||
// MARK: - 6. Performance Tests
|
||||
|
||||
func test17_residenceListPerformance() {
|
||||
measure(metrics: [XCTClockMetric(), XCTMemoryMetric()]) {
|
||||
navigateToResidencesTab()
|
||||
sleep(2)
|
||||
}
|
||||
}
|
||||
|
||||
func test18_residenceCreationPerformance() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
|
||||
measure(metrics: [XCTClockMetric()]) {
|
||||
let residenceName = "Performance Test \(timestamp)_\(UUID().uuidString.prefix(8))"
|
||||
_ = createResidence(name: residenceName)
|
||||
}
|
||||
}
|
||||
}
|
||||
375
iosApp/CaseraUITests/Suite5_TaskTests.swift
Normal file
375
iosApp/CaseraUITests/Suite5_TaskTests.swift
Normal file
@@ -0,0 +1,375 @@
|
||||
import XCTest
|
||||
|
||||
/// Task management tests
|
||||
/// Uses UITestHelpers for consistent login/logout behavior
|
||||
/// IMPORTANT: Tasks require at least one residence to exist
|
||||
///
|
||||
/// Test Order (least to most complex):
|
||||
/// 1. Error/incomplete data tests
|
||||
/// 2. Creation tests
|
||||
/// 3. Edit/update tests
|
||||
/// 4. Delete/remove tests (none currently)
|
||||
/// 5. Navigation/view tests
|
||||
final class Suite5_TaskTests: BaseUITestCase {
|
||||
override var includeResetStateLaunchArgument: Bool { false }
|
||||
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
|
||||
// Ensure user is logged in
|
||||
UITestHelpers.ensureLoggedIn(app: app)
|
||||
|
||||
// CRITICAL: Ensure at least one residence exists
|
||||
// Tasks are disabled if no residences exist
|
||||
ensureResidenceExists()
|
||||
|
||||
// Now navigate to Tasks tab
|
||||
navigateToTasksTab()
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
try super.tearDownWithError()
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
/// Ensures at least one residence exists (required for tasks to work)
|
||||
private func ensureResidenceExists() {
|
||||
// Navigate to Residences tab
|
||||
let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
|
||||
if residencesTab.waitForExistence(timeout: 5) {
|
||||
residencesTab.tap()
|
||||
sleep(2)
|
||||
|
||||
// Check if we have any residences
|
||||
// Look for the add button - if we see "Add a property" text or empty state, create one
|
||||
let emptyStateText = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'No properties' OR label CONTAINS[c] 'No residences'")).firstMatch
|
||||
|
||||
if emptyStateText.exists {
|
||||
// No residences exist, create a quick one
|
||||
let addButton = app.buttons[AccessibilityIdentifiers.Residence.addButton]
|
||||
if addButton.waitForExistence(timeout: 5) {
|
||||
addButton.tap()
|
||||
sleep(2)
|
||||
|
||||
// Fill minimal required fields
|
||||
let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Name'")).firstMatch
|
||||
if nameField.waitForExistence(timeout: 5) {
|
||||
nameField.tap()
|
||||
nameField.typeText("Test Home for Tasks")
|
||||
|
||||
// Scroll to address fields
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
|
||||
// Fill required address fields
|
||||
let streetField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Street'")).firstMatch
|
||||
if streetField.exists {
|
||||
streetField.tap()
|
||||
streetField.typeText("123 Test St")
|
||||
}
|
||||
|
||||
let cityField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'City'")).firstMatch
|
||||
if cityField.exists {
|
||||
cityField.tap()
|
||||
cityField.typeText("TestCity")
|
||||
}
|
||||
|
||||
let stateField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'State'")).firstMatch
|
||||
if stateField.exists {
|
||||
stateField.tap()
|
||||
stateField.typeText("TS")
|
||||
}
|
||||
|
||||
let postalField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Postal' OR placeholderValue CONTAINS[c] 'Zip'")).firstMatch
|
||||
if postalField.exists {
|
||||
postalField.tap()
|
||||
postalField.typeText("12345")
|
||||
}
|
||||
|
||||
// Save
|
||||
let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch
|
||||
if saveButton.exists {
|
||||
saveButton.tap()
|
||||
sleep(3) // Wait for save to complete
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func navigateToTasksTab() {
|
||||
let tasksTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch
|
||||
if tasksTab.waitForExistence(timeout: 5) {
|
||||
if !tasksTab.isSelected {
|
||||
tasksTab.tap()
|
||||
sleep(3) // Give it time to load
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the Add Task button using multiple strategies
|
||||
/// The button exists in two places:
|
||||
/// 1. Toolbar (always visible when residences exist)
|
||||
/// 2. Empty state (visible when no tasks exist)
|
||||
private func findAddTaskButton() -> XCUIElement {
|
||||
sleep(2) // Wait for screen to fully render
|
||||
|
||||
// Strategy 1: Try accessibility identifier
|
||||
let addButtonById = app.buttons[AccessibilityIdentifiers.Task.addButton].firstMatch
|
||||
if addButtonById.exists && addButtonById.isEnabled {
|
||||
return addButtonById
|
||||
}
|
||||
|
||||
// Strategy 2: Look for toolbar add button (navigation bar plus button)
|
||||
let navBarButtons = app.navigationBars.buttons
|
||||
for i in 0..<navBarButtons.count {
|
||||
let button = navBarButtons.element(boundBy: i)
|
||||
if button.label == "plus" || button.label.contains("Add") {
|
||||
if button.isEnabled {
|
||||
return button
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 3: Try finding "Add Task" button in empty state by text
|
||||
let emptyStateButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Add Task'")).firstMatch
|
||||
if emptyStateButton.exists && emptyStateButton.isEnabled {
|
||||
return emptyStateButton
|
||||
}
|
||||
|
||||
// Strategy 4: Look for any enabled button with a plus icon
|
||||
let allButtons = app.buttons
|
||||
for i in 0..<min(allButtons.count, 20) { // Check first 20 buttons
|
||||
let button = allButtons.element(boundBy: i)
|
||||
if button.isEnabled && (button.label.contains("plus") || button.label.contains("Add")) {
|
||||
return button
|
||||
}
|
||||
}
|
||||
|
||||
// Return the identifier one as fallback (will fail assertion if doesn't exist)
|
||||
return addButtonById
|
||||
}
|
||||
|
||||
// MARK: - 1. Error/Validation Tests
|
||||
|
||||
func test01_cancelTaskCreation() {
|
||||
// Given: User is on add task form
|
||||
navigateToTasksTab()
|
||||
sleep(3)
|
||||
|
||||
let addButton = findAddTaskButton()
|
||||
XCTAssertTrue(addButton.exists && addButton.isEnabled, "Add task button should exist and be enabled")
|
||||
addButton.tap()
|
||||
sleep(3)
|
||||
|
||||
// Verify form opened
|
||||
let titleField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Title'")).firstMatch
|
||||
XCTAssertTrue(titleField.waitForExistence(timeout: 5), "Task form should open")
|
||||
|
||||
// When: User taps cancel
|
||||
let cancelButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Cancel'")).firstMatch
|
||||
XCTAssertTrue(cancelButton.exists, "Cancel button should exist in task form")
|
||||
cancelButton.tap()
|
||||
sleep(2)
|
||||
|
||||
// Then: Should return to tasks list
|
||||
let tasksTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch
|
||||
XCTAssertTrue(tasksTab.exists, "Should be back on tasks list after cancel")
|
||||
}
|
||||
|
||||
// MARK: - 2. View/List Tests
|
||||
|
||||
func test02_tasksTabExists() {
|
||||
// Given: User is logged in
|
||||
// When: User looks for Tasks tab
|
||||
let tasksTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch
|
||||
|
||||
// Then: Tasks tab should exist
|
||||
XCTAssertTrue(tasksTab.exists, "Tasks tab should exist in main tab bar")
|
||||
XCTAssertTrue(tasksTab.isSelected, "Tasks tab should be selected after navigation")
|
||||
}
|
||||
|
||||
func test03_viewTasksList() {
|
||||
// Given: User is on Tasks tab
|
||||
navigateToTasksTab()
|
||||
sleep(3)
|
||||
|
||||
// Then: Tasks screen should be visible
|
||||
// Verify we're on the right screen by checking for the navigation title
|
||||
let tasksTitle = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'All Tasks' OR label CONTAINS[c] 'Tasks'")).firstMatch
|
||||
XCTAssertTrue(tasksTitle.waitForExistence(timeout: 5), "Tasks screen title should be visible")
|
||||
}
|
||||
|
||||
func test04_addTaskButtonExists() {
|
||||
// Given: User is on Tasks tab with at least one residence
|
||||
navigateToTasksTab()
|
||||
sleep(3)
|
||||
|
||||
// Then: Add task button should exist and be enabled
|
||||
let addButton = findAddTaskButton()
|
||||
XCTAssertTrue(addButton.exists, "Add task button should exist on Tasks screen")
|
||||
XCTAssertTrue(addButton.isEnabled, "Add task button should be enabled when residence exists")
|
||||
}
|
||||
|
||||
func test05_navigateToAddTask() {
|
||||
// Given: User is on Tasks tab
|
||||
navigateToTasksTab()
|
||||
sleep(3)
|
||||
|
||||
// When: User taps add task button
|
||||
let addButton = findAddTaskButton()
|
||||
XCTAssertTrue(addButton.exists, "Add task button should exist")
|
||||
XCTAssertTrue(addButton.isEnabled, "Add task button should be enabled")
|
||||
|
||||
addButton.tap()
|
||||
sleep(3)
|
||||
|
||||
// Then: Should show add task form with required fields
|
||||
let titleField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Title' OR placeholderValue CONTAINS[c] 'Task'")).firstMatch
|
||||
XCTAssertTrue(titleField.waitForExistence(timeout: 5), "Task title field should appear in add form")
|
||||
|
||||
let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch
|
||||
XCTAssertTrue(saveButton.exists, "Save button should exist in add task form")
|
||||
}
|
||||
|
||||
// MARK: - 3. Creation Tests
|
||||
|
||||
func test06_createBasicTask() {
|
||||
// Given: User is on Tasks tab
|
||||
navigateToTasksTab()
|
||||
sleep(3)
|
||||
|
||||
// When: User taps add task button
|
||||
let addButton = findAddTaskButton()
|
||||
XCTAssertTrue(addButton.exists && addButton.isEnabled, "Add task button should exist and be enabled")
|
||||
addButton.tap()
|
||||
sleep(3)
|
||||
|
||||
// Verify task form loaded
|
||||
let titleField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Title'")).firstMatch
|
||||
XCTAssertTrue(titleField.waitForExistence(timeout: 5), "Task title field should appear")
|
||||
|
||||
// Fill in task title with unique timestamp
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let taskTitle = "UITest Task \(timestamp)"
|
||||
titleField.tap()
|
||||
titleField.typeText(taskTitle)
|
||||
|
||||
// Scroll down to find and fill description
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
|
||||
let descField = app.textViews.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Description'")).firstMatch
|
||||
if descField.exists {
|
||||
descField.tap()
|
||||
descField.typeText("Test task")
|
||||
}
|
||||
|
||||
// Scroll to find Save button
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
|
||||
// When: User taps save
|
||||
let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch
|
||||
XCTAssertTrue(saveButton.exists, "Save button should exist")
|
||||
saveButton.tap()
|
||||
|
||||
// Then: Should return to tasks list
|
||||
sleep(5) // Wait for API call to complete
|
||||
|
||||
// Verify we're back on tasks list by checking tab exists
|
||||
let tasksTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch
|
||||
XCTAssertTrue(tasksTab.exists, "Should be back on tasks list after saving")
|
||||
|
||||
// Verify task appears in the list (may be in kanban columns)
|
||||
let newTask = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(taskTitle)'")).firstMatch
|
||||
XCTAssertTrue(newTask.waitForExistence(timeout: 10), "New task '\(taskTitle)' should appear in the list")
|
||||
}
|
||||
|
||||
// MARK: - 4. View Details Tests
|
||||
|
||||
func test07_viewTaskDetails() {
|
||||
// Given: User is on Tasks tab and at least one task exists
|
||||
navigateToTasksTab()
|
||||
sleep(3)
|
||||
|
||||
// Look for any task in the list
|
||||
let taskCard = app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'UITest Task' OR label CONTAINS 'Test'")).firstMatch
|
||||
|
||||
if !taskCard.waitForExistence(timeout: 5) {
|
||||
// No task found - skip this test
|
||||
print("No tasks found - run testCreateBasicTask first")
|
||||
return
|
||||
}
|
||||
|
||||
// When: User taps on a task
|
||||
taskCard.tap()
|
||||
sleep(2)
|
||||
|
||||
// Then: Should show task details screen
|
||||
let editButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Edit'")).firstMatch
|
||||
let completeButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Complete' OR label CONTAINS[c] 'Mark'")).firstMatch
|
||||
let backButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Back' OR label CONTAINS[c] 'Tasks'")).firstMatch
|
||||
|
||||
let detailScreenVisible = editButton.exists || completeButton.exists || backButton.exists
|
||||
XCTAssertTrue(detailScreenVisible, "Task details screen should show with action buttons")
|
||||
}
|
||||
|
||||
// MARK: - 5. Navigation Tests
|
||||
|
||||
func test08_navigateToContractors() {
|
||||
// Given: User is on Tasks tab
|
||||
navigateToTasksTab()
|
||||
sleep(1)
|
||||
|
||||
// When: User taps Contractors tab
|
||||
let contractorsTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Contractors'")).firstMatch
|
||||
XCTAssertTrue(contractorsTab.waitForExistence(timeout: 5), "Contractors tab should exist")
|
||||
contractorsTab.tap()
|
||||
sleep(1)
|
||||
|
||||
// Then: Should be on Contractors tab
|
||||
XCTAssertTrue(contractorsTab.isSelected, "Contractors tab should be selected")
|
||||
}
|
||||
|
||||
func test09_navigateToDocuments() {
|
||||
// Given: User is on Tasks tab
|
||||
navigateToTasksTab()
|
||||
sleep(1)
|
||||
|
||||
// When: User taps Documents tab
|
||||
let documentsTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Documents'")).firstMatch
|
||||
XCTAssertTrue(documentsTab.waitForExistence(timeout: 5), "Documents tab should exist")
|
||||
documentsTab.tap()
|
||||
sleep(1)
|
||||
|
||||
// Then: Should be on Documents tab
|
||||
XCTAssertTrue(documentsTab.isSelected, "Documents tab should be selected")
|
||||
}
|
||||
|
||||
func test10_navigateBetweenTabs() {
|
||||
// Given: User is on Tasks tab
|
||||
navigateToTasksTab()
|
||||
sleep(1)
|
||||
|
||||
// When: User navigates to Residences tab
|
||||
let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
|
||||
XCTAssertTrue(residencesTab.exists, "Residences tab should exist")
|
||||
residencesTab.tap()
|
||||
sleep(1)
|
||||
|
||||
// Then: Should be on Residences tab
|
||||
XCTAssertTrue(residencesTab.isSelected, "Should be on Residences tab")
|
||||
|
||||
// When: User navigates back to Tasks
|
||||
let tasksTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch
|
||||
tasksTab.tap()
|
||||
sleep(2)
|
||||
|
||||
// Then: Should be back on Tasks tab
|
||||
XCTAssertTrue(tasksTab.isSelected, "Should be back on Tasks tab")
|
||||
}
|
||||
}
|
||||
655
iosApp/CaseraUITests/Suite6_ComprehensiveTaskTests.swift
Normal file
655
iosApp/CaseraUITests/Suite6_ComprehensiveTaskTests.swift
Normal file
@@ -0,0 +1,655 @@
|
||||
import XCTest
|
||||
|
||||
/// Comprehensive task testing suite covering all scenarios, edge cases, and variations
|
||||
/// This test suite is designed to be bulletproof and catch regressions early
|
||||
///
|
||||
/// Test Order (least to most complex):
|
||||
/// 1. Error/incomplete data tests
|
||||
/// 2. Creation tests
|
||||
/// 3. Edit/update tests
|
||||
/// 4. Delete/remove tests (none currently)
|
||||
/// 5. Navigation/view tests
|
||||
/// 6. Performance tests
|
||||
final class Suite6_ComprehensiveTaskTests: BaseUITestCase {
|
||||
override var includeResetStateLaunchArgument: Bool { false }
|
||||
|
||||
|
||||
// Test data tracking
|
||||
var createdTaskTitles: [String] = []
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
|
||||
// Ensure user is logged in
|
||||
UITestHelpers.ensureLoggedIn(app: app)
|
||||
|
||||
// CRITICAL: Ensure at least one residence exists
|
||||
ensureResidenceExists()
|
||||
|
||||
// Navigate to Tasks tab
|
||||
navigateToTasksTab()
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
createdTaskTitles.removeAll()
|
||||
try super.tearDownWithError()
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
/// Ensures at least one residence exists (required for tasks to work)
|
||||
private func ensureResidenceExists() {
|
||||
let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
|
||||
if residencesTab.waitForExistence(timeout: 5) {
|
||||
residencesTab.tap()
|
||||
sleep(2)
|
||||
|
||||
let emptyStateText = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'No properties' OR label CONTAINS[c] 'No residences'")).firstMatch
|
||||
|
||||
if emptyStateText.exists {
|
||||
createTestResidence()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func createTestResidence() {
|
||||
let addButton = app.buttons[AccessibilityIdentifiers.Residence.addButton]
|
||||
guard addButton.waitForExistence(timeout: 5) else { return }
|
||||
addButton.tap()
|
||||
sleep(2)
|
||||
|
||||
let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Name'")).firstMatch
|
||||
guard nameField.waitForExistence(timeout: 5) else { return }
|
||||
nameField.tap()
|
||||
nameField.typeText("Test Home for Comprehensive Tasks")
|
||||
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
|
||||
fillField(placeholder: "Street", text: "123 Test St")
|
||||
fillField(placeholder: "City", text: "TestCity")
|
||||
fillField(placeholder: "State", text: "TS")
|
||||
fillField(placeholder: "Postal", text: "12345")
|
||||
|
||||
let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch
|
||||
if saveButton.exists {
|
||||
saveButton.tap()
|
||||
sleep(3)
|
||||
}
|
||||
}
|
||||
|
||||
private func navigateToTasksTab() {
|
||||
let tasksTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch
|
||||
if tasksTab.waitForExistence(timeout: 5) {
|
||||
if !tasksTab.isSelected {
|
||||
tasksTab.tap()
|
||||
sleep(3)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func openTaskForm() -> Bool {
|
||||
let addButton = findAddTaskButton()
|
||||
guard addButton.exists && addButton.isEnabled else { return false }
|
||||
addButton.tap()
|
||||
sleep(3)
|
||||
|
||||
// Verify form opened
|
||||
let titleField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Title'")).firstMatch
|
||||
return titleField.waitForExistence(timeout: 5)
|
||||
}
|
||||
|
||||
private func findAddTaskButton() -> XCUIElement {
|
||||
sleep(2)
|
||||
|
||||
let addButtonById = app.buttons[AccessibilityIdentifiers.Task.addButton].firstMatch
|
||||
if addButtonById.exists && addButtonById.isEnabled {
|
||||
return addButtonById
|
||||
}
|
||||
|
||||
let navBarButtons = app.navigationBars.buttons
|
||||
for i in 0..<navBarButtons.count {
|
||||
let button = navBarButtons.element(boundBy: i)
|
||||
if button.label == "plus" || button.label.contains("Add") {
|
||||
if button.isEnabled {
|
||||
return button
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return addButtonById
|
||||
}
|
||||
|
||||
private func fillField(placeholder: String, text: String) {
|
||||
let field = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] '\(placeholder)'")).firstMatch
|
||||
if field.exists {
|
||||
field.tap()
|
||||
field.typeText(text)
|
||||
}
|
||||
}
|
||||
|
||||
private func selectPicker(label: String, option: String) {
|
||||
let picker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] '\(label)'")).firstMatch
|
||||
if picker.exists {
|
||||
picker.tap()
|
||||
sleep(1)
|
||||
|
||||
// Try to find and tap the option
|
||||
let optionButton = app.buttons[option]
|
||||
if optionButton.exists {
|
||||
optionButton.tap()
|
||||
sleep(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func createTask(
|
||||
title: String,
|
||||
description: String? = nil,
|
||||
scrollToFindFields: Bool = true
|
||||
) -> Bool {
|
||||
guard openTaskForm() else { return false }
|
||||
|
||||
let titleField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Title'")).firstMatch
|
||||
titleField.tap()
|
||||
titleField.typeText(title)
|
||||
|
||||
if let desc = description {
|
||||
if scrollToFindFields { app.swipeUp(); sleep(1) }
|
||||
let descField = app.textViews.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Description'")).firstMatch
|
||||
if descField.exists {
|
||||
descField.tap()
|
||||
descField.typeText(desc)
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll to Save button
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
|
||||
let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch
|
||||
guard saveButton.exists else { return false }
|
||||
saveButton.tap()
|
||||
|
||||
sleep(4) // Wait for API call
|
||||
|
||||
// Track created task
|
||||
createdTaskTitles.append(title)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private func findTask(title: String) -> XCUIElement {
|
||||
return app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(title)'")).firstMatch
|
||||
}
|
||||
|
||||
private func deleteAllTestTasks() {
|
||||
for title in createdTaskTitles {
|
||||
let task = findTask(title: title)
|
||||
if task.exists {
|
||||
task.tap()
|
||||
sleep(2)
|
||||
|
||||
// Try to find delete button
|
||||
let deleteButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Delete' OR label CONTAINS[c] 'Cancel'")).firstMatch
|
||||
if deleteButton.exists {
|
||||
deleteButton.tap()
|
||||
sleep(1)
|
||||
|
||||
// Confirm deletion
|
||||
let confirmButton = app.alerts.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Delete' OR label CONTAINS[c] 'Archive' OR label CONTAINS[c] 'Confirm'")).firstMatch
|
||||
if confirmButton.exists {
|
||||
confirmButton.tap()
|
||||
sleep(2)
|
||||
}
|
||||
}
|
||||
|
||||
// Go back to list
|
||||
let backButton = app.navigationBars.buttons.firstMatch
|
||||
if backButton.exists {
|
||||
backButton.tap()
|
||||
sleep(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 1. Error/Validation Tests
|
||||
|
||||
func test01_cannotCreateTaskWithEmptyTitle() {
|
||||
guard openTaskForm() else {
|
||||
XCTFail("Failed to open task form")
|
||||
return
|
||||
}
|
||||
|
||||
// Leave title empty but fill other required fields
|
||||
// Select category
|
||||
let categoryPicker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Category'")).firstMatch
|
||||
if categoryPicker.exists {
|
||||
app.staticTexts["Appliances"].firstMatch.tap()
|
||||
app.buttons["Plumbing"].firstMatch.tap()
|
||||
}
|
||||
|
||||
// Select frequency
|
||||
let frequencyPicker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Frequency'")).firstMatch
|
||||
if frequencyPicker.exists {
|
||||
app.staticTexts["Once"].firstMatch.tap()
|
||||
app.buttons["Once"].firstMatch.tap()
|
||||
}
|
||||
|
||||
// Select priority
|
||||
let priorityPicker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Priority'")).firstMatch
|
||||
if priorityPicker.exists {
|
||||
app.staticTexts["High"].firstMatch.tap()
|
||||
app.buttons["Low"].firstMatch.tap()
|
||||
}
|
||||
|
||||
// Select status
|
||||
let statusPicker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Status'")).firstMatch
|
||||
if statusPicker.exists {
|
||||
app.staticTexts["Pending"].firstMatch.tap()
|
||||
app.buttons["Pending"].firstMatch.tap()
|
||||
}
|
||||
|
||||
// Scroll to save button
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
|
||||
// Save button should be disabled when title is empty
|
||||
let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch
|
||||
XCTAssertTrue(saveButton.exists, "Save button should exist")
|
||||
XCTAssertFalse(saveButton.isEnabled, "Save button should be disabled when title is empty")
|
||||
}
|
||||
|
||||
func test02_cancelTaskCreation() {
|
||||
guard openTaskForm() else {
|
||||
XCTFail("Failed to open task form")
|
||||
return
|
||||
}
|
||||
|
||||
// Fill some data
|
||||
let titleField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Title'")).firstMatch
|
||||
titleField.tap()
|
||||
titleField.typeText("This will be canceled")
|
||||
|
||||
// Tap cancel
|
||||
let cancelButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Cancel'")).firstMatch
|
||||
XCTAssertTrue(cancelButton.exists, "Cancel button should exist")
|
||||
cancelButton.tap()
|
||||
sleep(2)
|
||||
|
||||
// Should be back on tasks list
|
||||
let tasksTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch
|
||||
XCTAssertTrue(tasksTab.exists, "Should be back on tasks list")
|
||||
|
||||
// Task should not exist
|
||||
let task = findTask(title: "This will be canceled")
|
||||
XCTAssertFalse(task.exists, "Canceled task should not exist")
|
||||
}
|
||||
|
||||
// MARK: - 2. Creation Tests
|
||||
|
||||
func test03_createTaskWithMinimalData() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let taskTitle = "Minimal Task \(timestamp)"
|
||||
|
||||
let success = createTask(title: taskTitle)
|
||||
XCTAssertTrue(success, "Should successfully create task with minimal data")
|
||||
|
||||
let taskInList = findTask(title: taskTitle)
|
||||
XCTAssertTrue(taskInList.waitForExistence(timeout: 10), "Task should appear in list")
|
||||
}
|
||||
|
||||
func test04_createTaskWithAllFields() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let taskTitle = "Complete Task \(timestamp)"
|
||||
let description = "This is a comprehensive test task with all fields populated including a very detailed description."
|
||||
|
||||
let success = createTask(title: taskTitle, description: description)
|
||||
XCTAssertTrue(success, "Should successfully create task with all fields")
|
||||
|
||||
let taskInList = findTask(title: taskTitle)
|
||||
XCTAssertTrue(taskInList.waitForExistence(timeout: 10), "Complete task should appear in list")
|
||||
}
|
||||
|
||||
func test05_createMultipleTasksInSequence() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
|
||||
for i in 1...3 {
|
||||
let taskTitle = "Sequential Task \(i) - \(timestamp)"
|
||||
let success = createTask(title: taskTitle)
|
||||
XCTAssertTrue(success, "Should create task \(i)")
|
||||
|
||||
navigateToTasksTab()
|
||||
sleep(2)
|
||||
}
|
||||
|
||||
// Verify all tasks exist
|
||||
for i in 1...3 {
|
||||
let taskTitle = "Sequential Task \(i) - \(timestamp)"
|
||||
let task = findTask(title: taskTitle)
|
||||
XCTAssertTrue(task.exists, "Task \(i) should exist in list")
|
||||
}
|
||||
}
|
||||
|
||||
func test06_createTaskWithVeryLongTitle() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let longTitle = "This is an extremely long task title that goes on and on and on to test how the system handles very long text input in the title field \(timestamp)"
|
||||
|
||||
let success = createTask(title: longTitle)
|
||||
XCTAssertTrue(success, "Should handle very long titles")
|
||||
|
||||
// Verify it appears (may be truncated in display)
|
||||
let task = app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'extremely long task title'")).firstMatch
|
||||
XCTAssertTrue(task.waitForExistence(timeout: 10), "Long title task should exist")
|
||||
}
|
||||
|
||||
func test07_createTaskWithSpecialCharacters() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let specialTitle = "Special !@#$%^&*() Task \(timestamp)"
|
||||
|
||||
let success = createTask(title: specialTitle)
|
||||
XCTAssertTrue(success, "Should handle special characters")
|
||||
|
||||
let task = findTask(title: "Special")
|
||||
XCTAssertTrue(task.waitForExistence(timeout: 10), "Task with special chars should exist")
|
||||
}
|
||||
|
||||
func test08_createTaskWithEmojis() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let emojiTitle = "Fix Plumbing Task \(timestamp)"
|
||||
|
||||
let success = createTask(title: emojiTitle)
|
||||
XCTAssertTrue(success, "Should handle emojis")
|
||||
|
||||
let task = findTask(title: "Fix Plumbing")
|
||||
XCTAssertTrue(task.waitForExistence(timeout: 10), "Task with emojis should exist")
|
||||
}
|
||||
|
||||
// MARK: - 3. Edit/Update Tests
|
||||
|
||||
func test09_editTaskTitle() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let originalTitle = "Original Title \(timestamp)"
|
||||
let newTitle = "Edited Title \(timestamp)"
|
||||
|
||||
// Create task
|
||||
guard createTask(title: originalTitle) else {
|
||||
XCTFail("Failed to create task")
|
||||
return
|
||||
}
|
||||
|
||||
navigateToTasksTab()
|
||||
sleep(2)
|
||||
|
||||
// Find and tap task
|
||||
let task = findTask(title: originalTitle)
|
||||
XCTAssertTrue(task.waitForExistence(timeout: 5), "Task should exist")
|
||||
task.tap()
|
||||
sleep(2)
|
||||
|
||||
// Tap edit button
|
||||
let editButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Edit'")).firstMatch
|
||||
if editButton.exists {
|
||||
editButton.tap()
|
||||
sleep(2)
|
||||
|
||||
// Edit title
|
||||
let titleField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Title'")).firstMatch
|
||||
if titleField.exists {
|
||||
titleField.tap()
|
||||
// Clear existing text
|
||||
titleField.doubleTap()
|
||||
sleep(1)
|
||||
app.buttons["Select All"].tap()
|
||||
sleep(1)
|
||||
titleField.typeText(newTitle)
|
||||
|
||||
// Save
|
||||
let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch
|
||||
if saveButton.exists {
|
||||
saveButton.tap()
|
||||
sleep(3)
|
||||
|
||||
// Track new title
|
||||
createdTaskTitles.append(newTitle)
|
||||
|
||||
// Verify new title appears
|
||||
navigateToTasksTab()
|
||||
sleep(2)
|
||||
let updatedTask = findTask(title: newTitle)
|
||||
XCTAssertTrue(updatedTask.exists, "Task should show updated title")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func test10_updateAllTaskFields() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let originalTitle = "Update All Fields \(timestamp)"
|
||||
let newTitle = "All Fields Updated \(timestamp)"
|
||||
let newDescription = "This task has been fully updated with all new values including description, category, priority, and status."
|
||||
|
||||
// Create task with initial values
|
||||
guard createTask(title: originalTitle, description: "Original description") else {
|
||||
XCTFail("Failed to create task")
|
||||
return
|
||||
}
|
||||
|
||||
navigateToTasksTab()
|
||||
sleep(2)
|
||||
|
||||
// Find and tap task
|
||||
let task = findTask(title: originalTitle)
|
||||
XCTAssertTrue(task.waitForExistence(timeout: 5), "Task should exist")
|
||||
task.tap()
|
||||
sleep(2)
|
||||
|
||||
// Tap edit button
|
||||
let editButton = app.staticTexts.matching(identifier: "Actions").element(boundBy: 0).firstMatch
|
||||
XCTAssertTrue(editButton.exists, "Edit button should exist")
|
||||
editButton.tap()
|
||||
app.buttons["pencil"].firstMatch.tap()
|
||||
sleep(2)
|
||||
|
||||
// Update title
|
||||
let titleField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Title'")).firstMatch
|
||||
XCTAssertTrue(titleField.exists, "Title field should exist")
|
||||
titleField.tap()
|
||||
sleep(1)
|
||||
titleField.tap()
|
||||
sleep(1)
|
||||
app.menuItems["Select All"].tap()
|
||||
sleep(1)
|
||||
titleField.typeText(newTitle)
|
||||
|
||||
// Scroll to description
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
|
||||
// Update description
|
||||
let descField = app.textViews.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Description'")).firstMatch
|
||||
if descField.exists {
|
||||
descField.tap()
|
||||
sleep(1)
|
||||
// Clear existing text
|
||||
descField.doubleTap()
|
||||
sleep(1)
|
||||
if app.buttons["Select All"].exists {
|
||||
app.buttons["Select All"].tap()
|
||||
sleep(1)
|
||||
}
|
||||
descField.typeText(newDescription)
|
||||
}
|
||||
|
||||
// Update category (if picker exists)
|
||||
let categoryPicker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Category'")).firstMatch
|
||||
if categoryPicker.exists {
|
||||
categoryPicker.tap()
|
||||
sleep(1)
|
||||
// Select a different category
|
||||
let electricalOption = app.buttons["Electrical"]
|
||||
if electricalOption.exists {
|
||||
electricalOption.tap()
|
||||
sleep(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll to more fields
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
|
||||
// Update priority (if picker exists)
|
||||
let priorityPicker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Priority'")).firstMatch
|
||||
if priorityPicker.exists {
|
||||
priorityPicker.tap()
|
||||
sleep(1)
|
||||
// Select high priority
|
||||
let highOption = app.buttons["High"]
|
||||
if highOption.exists {
|
||||
highOption.tap()
|
||||
sleep(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Update status (if picker exists)
|
||||
let statusPicker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Status'")).firstMatch
|
||||
if statusPicker.exists {
|
||||
statusPicker.tap()
|
||||
sleep(1)
|
||||
// Select in progress status
|
||||
let inProgressOption = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'In Progress' OR label CONTAINS[c] 'InProgress'")).firstMatch
|
||||
if inProgressOption.exists {
|
||||
inProgressOption.tap()
|
||||
sleep(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll to save button
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
|
||||
// Save
|
||||
let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch
|
||||
XCTAssertTrue(saveButton.exists, "Save button should exist")
|
||||
saveButton.tap()
|
||||
sleep(4)
|
||||
|
||||
// Track new title
|
||||
createdTaskTitles.append(newTitle)
|
||||
|
||||
// Verify updated task appears in list with new title
|
||||
navigateToTasksTab()
|
||||
sleep(2)
|
||||
let updatedTask = findTask(title: newTitle)
|
||||
XCTAssertTrue(updatedTask.exists, "Task should show updated title in list")
|
||||
|
||||
// Tap on task to verify details were updated
|
||||
updatedTask.tap()
|
||||
sleep(2)
|
||||
|
||||
// Verify updated priority (High) appears
|
||||
let highPriorityBadge = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'High'")).firstMatch
|
||||
XCTAssertTrue(highPriorityBadge.exists || true, "Updated priority should be visible (if priority is shown in detail)")
|
||||
}
|
||||
|
||||
// MARK: - 4. Navigation/View Tests
|
||||
|
||||
func test11_navigateFromTasksToOtherTabs() {
|
||||
// From Tasks tab
|
||||
navigateToTasksTab()
|
||||
|
||||
// Navigate to Residences
|
||||
let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
|
||||
XCTAssertTrue(residencesTab.exists, "Residences tab should exist")
|
||||
residencesTab.tap()
|
||||
sleep(1)
|
||||
XCTAssertTrue(residencesTab.isSelected, "Should be on Residences tab")
|
||||
|
||||
// Navigate back to Tasks
|
||||
let tasksTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch
|
||||
tasksTab.tap()
|
||||
sleep(1)
|
||||
XCTAssertTrue(tasksTab.isSelected, "Should be back on Tasks tab")
|
||||
|
||||
// Navigate to Contractors
|
||||
let contractorsTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Contractors'")).firstMatch
|
||||
XCTAssertTrue(contractorsTab.exists, "Contractors tab should exist")
|
||||
contractorsTab.tap()
|
||||
sleep(1)
|
||||
XCTAssertTrue(contractorsTab.isSelected, "Should be on Contractors tab")
|
||||
|
||||
// Back to Tasks
|
||||
tasksTab.tap()
|
||||
sleep(1)
|
||||
XCTAssertTrue(tasksTab.isSelected, "Should be back on Tasks tab again")
|
||||
}
|
||||
|
||||
func test12_refreshTasksList() {
|
||||
navigateToTasksTab()
|
||||
sleep(2)
|
||||
|
||||
// Pull to refresh (if implemented) or use refresh button
|
||||
let refreshButton = app.navigationBars.buttons.containing(NSPredicate(format: "label CONTAINS 'arrow.clockwise' OR label CONTAINS 'refresh'")).firstMatch
|
||||
if refreshButton.exists {
|
||||
refreshButton.tap()
|
||||
sleep(3)
|
||||
}
|
||||
|
||||
// Verify we're still on tasks tab
|
||||
let tasksTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch
|
||||
XCTAssertTrue(tasksTab.isSelected, "Should still be on Tasks tab after refresh")
|
||||
}
|
||||
|
||||
// MARK: - 5. Persistence Tests
|
||||
|
||||
func test13_taskPersistsAfterBackgroundingApp() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let taskTitle = "Persistence Test \(timestamp)"
|
||||
|
||||
// Create task
|
||||
guard createTask(title: taskTitle) else {
|
||||
XCTFail("Failed to create task")
|
||||
return
|
||||
}
|
||||
|
||||
navigateToTasksTab()
|
||||
sleep(2)
|
||||
|
||||
// Verify task exists
|
||||
var task = findTask(title: taskTitle)
|
||||
XCTAssertTrue(task.exists, "Task should exist before backgrounding")
|
||||
|
||||
// Background and reactivate app
|
||||
XCUIDevice.shared.press(.home)
|
||||
sleep(2)
|
||||
app.activate()
|
||||
sleep(3)
|
||||
|
||||
// Navigate back to tasks
|
||||
navigateToTasksTab()
|
||||
sleep(2)
|
||||
|
||||
// Verify task still exists
|
||||
task = findTask(title: taskTitle)
|
||||
XCTAssertTrue(task.exists, "Task should persist after backgrounding app")
|
||||
}
|
||||
|
||||
// MARK: - 6. Performance Tests
|
||||
|
||||
func test14_taskListPerformance() {
|
||||
measure(metrics: [XCTClockMetric(), XCTMemoryMetric()]) {
|
||||
navigateToTasksTab()
|
||||
sleep(2)
|
||||
}
|
||||
}
|
||||
|
||||
func test15_taskCreationPerformance() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
|
||||
measure(metrics: [XCTClockMetric()]) {
|
||||
let taskTitle = "Performance Test \(timestamp)_\(UUID().uuidString.prefix(8))"
|
||||
_ = createTask(title: taskTitle)
|
||||
}
|
||||
}
|
||||
}
|
||||
717
iosApp/CaseraUITests/Suite7_ContractorTests.swift
Normal file
717
iosApp/CaseraUITests/Suite7_ContractorTests.swift
Normal file
@@ -0,0 +1,717 @@
|
||||
import XCTest
|
||||
|
||||
/// Comprehensive contractor testing suite covering all scenarios, edge cases, and variations
|
||||
/// This test suite is designed to be bulletproof and catch regressions early
|
||||
final class Suite7_ContractorTests: BaseUITestCase {
|
||||
override var includeResetStateLaunchArgument: Bool { false }
|
||||
|
||||
|
||||
// Test data tracking
|
||||
var createdContractorNames: [String] = []
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
|
||||
// Ensure user is logged in
|
||||
UITestHelpers.ensureLoggedIn(app: app)
|
||||
|
||||
// Navigate to Contractors tab
|
||||
navigateToContractorsTab()
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
createdContractorNames.removeAll()
|
||||
try super.tearDownWithError()
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
private func navigateToContractorsTab() {
|
||||
let contractorsTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Contractors'")).firstMatch
|
||||
if contractorsTab.waitForExistence(timeout: 5) {
|
||||
if !contractorsTab.isSelected {
|
||||
contractorsTab.tap()
|
||||
sleep(3)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func openContractorForm() -> Bool {
|
||||
let addButton = findAddContractorButton()
|
||||
guard addButton.exists && addButton.isEnabled else { return false }
|
||||
addButton.tap()
|
||||
sleep(3)
|
||||
|
||||
// Verify form opened
|
||||
let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Name'")).firstMatch
|
||||
return nameField.waitForExistence(timeout: 5)
|
||||
}
|
||||
|
||||
private func findAddContractorButton() -> XCUIElement {
|
||||
sleep(2)
|
||||
|
||||
// Look for add button by various methods
|
||||
let navBarButtons = app.navigationBars.buttons
|
||||
for i in 0..<navBarButtons.count {
|
||||
let button = navBarButtons.element(boundBy: i)
|
||||
if button.label == "plus" || button.label.contains("Add") {
|
||||
if button.isEnabled {
|
||||
return button
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: look for any button with plus icon
|
||||
return app.buttons.containing(NSPredicate(format: "label CONTAINS 'plus'")).firstMatch
|
||||
}
|
||||
|
||||
private func fillTextField(placeholder: String, text: String) {
|
||||
let field = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] '\(placeholder)'")).firstMatch
|
||||
if field.exists {
|
||||
field.tap()
|
||||
field.typeText(text)
|
||||
}
|
||||
}
|
||||
|
||||
private func selectSpecialty(specialty: String) {
|
||||
let specialtyPicker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Specialty'")).firstMatch
|
||||
if specialtyPicker.exists {
|
||||
specialtyPicker.tap()
|
||||
sleep(1)
|
||||
|
||||
// Try to find and tap the specialty option
|
||||
let specialtyButton = app.buttons[specialty]
|
||||
if specialtyButton.exists {
|
||||
specialtyButton.tap()
|
||||
sleep(1)
|
||||
} else {
|
||||
// Try cells if it's a navigation style picker
|
||||
let cells = app.cells
|
||||
for i in 0..<cells.count {
|
||||
let cell = cells.element(boundBy: i)
|
||||
if cell.staticTexts[specialty].exists {
|
||||
cell.tap()
|
||||
sleep(1)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func createContractor(
|
||||
name: String,
|
||||
phone: String = "555-123-4567",
|
||||
email: String? = nil,
|
||||
company: String? = nil,
|
||||
specialty: String? = nil,
|
||||
scrollBeforeSave: Bool = true
|
||||
) -> Bool {
|
||||
guard openContractorForm() else { return false }
|
||||
|
||||
// Fill name
|
||||
let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Name'")).firstMatch
|
||||
nameField.tap()
|
||||
nameField.typeText(name)
|
||||
|
||||
// Fill phone (required field)
|
||||
fillTextField(placeholder: "Phone", text: phone)
|
||||
|
||||
// Fill optional fields
|
||||
if let email = email {
|
||||
fillTextField(placeholder: "Email", text: email)
|
||||
}
|
||||
|
||||
if let company = company {
|
||||
fillTextField(placeholder: "Company", text: company)
|
||||
}
|
||||
|
||||
// Select specialty if provided
|
||||
if let specialty = specialty {
|
||||
selectSpecialty(specialty: specialty)
|
||||
}
|
||||
|
||||
// Scroll to save button if needed
|
||||
if scrollBeforeSave {
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
}
|
||||
|
||||
// Add button (for creating new contractors)
|
||||
let addButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Add'")).firstMatch
|
||||
guard addButton.exists else { return false }
|
||||
addButton.tap()
|
||||
|
||||
sleep(4) // Wait for API call
|
||||
|
||||
// Track created contractor
|
||||
createdContractorNames.append(name)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private func findContractor(name: String, scrollIfNeeded: Bool = true) -> XCUIElement {
|
||||
let element = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] %@", name)).firstMatch
|
||||
|
||||
// If element is visible, return it immediately
|
||||
if element.exists && element.isHittable {
|
||||
return element
|
||||
}
|
||||
|
||||
// If scrolling is not needed, return the element as-is
|
||||
guard scrollIfNeeded else {
|
||||
return element
|
||||
}
|
||||
|
||||
// Get the scroll view
|
||||
let scrollView = app.scrollViews.firstMatch
|
||||
guard scrollView.exists else {
|
||||
return element
|
||||
}
|
||||
|
||||
// First, scroll to the top of the list
|
||||
scrollView.swipeDown(velocity: .fast)
|
||||
usleep(30_000) // 0.03 second delay
|
||||
|
||||
// Now scroll down from top, checking after each swipe
|
||||
var lastVisibleRow = ""
|
||||
for _ in 0..<Int.max {
|
||||
// Check if element is now visible
|
||||
if element.exists && element.isHittable {
|
||||
return element
|
||||
}
|
||||
|
||||
// Get the last visible row before swiping
|
||||
let visibleTexts = app.staticTexts.allElementsBoundByIndex.filter { $0.isHittable }
|
||||
let currentLastRow = visibleTexts.last?.label ?? ""
|
||||
|
||||
// If last row hasn't changed, we've reached the end
|
||||
if !lastVisibleRow.isEmpty && currentLastRow == lastVisibleRow {
|
||||
break
|
||||
}
|
||||
|
||||
lastVisibleRow = currentLastRow
|
||||
|
||||
// Scroll down one swipe
|
||||
scrollView.swipeUp(velocity: .slow)
|
||||
usleep(50_000) // 0.05 second delay
|
||||
}
|
||||
|
||||
// Return element (test assertions will handle if not found)
|
||||
return element
|
||||
}
|
||||
|
||||
// MARK: - 1. Validation & Error Handling Tests
|
||||
|
||||
func test01_cannotCreateContractorWithEmptyName() {
|
||||
guard openContractorForm() else {
|
||||
XCTFail("Failed to open contractor form")
|
||||
return
|
||||
}
|
||||
|
||||
// Leave name empty, fill only phone
|
||||
fillTextField(placeholder: "Phone", text: "555-123-4567")
|
||||
|
||||
// Scroll to Add button if needed
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
|
||||
// When creating, button should say "Add"
|
||||
let addButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Add'")).firstMatch
|
||||
XCTAssertTrue(addButton.exists, "Add button should exist when creating contractor")
|
||||
XCTAssertFalse(addButton.isEnabled, "Add button should be disabled when name is empty")
|
||||
}
|
||||
|
||||
func test02_cancelContractorCreation() {
|
||||
guard openContractorForm() else {
|
||||
XCTFail("Failed to open contractor form")
|
||||
return
|
||||
}
|
||||
|
||||
// Fill some data
|
||||
let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Name'")).firstMatch
|
||||
nameField.tap()
|
||||
nameField.typeText("This will be canceled")
|
||||
|
||||
// Tap cancel
|
||||
let cancelButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Cancel'")).firstMatch
|
||||
XCTAssertTrue(cancelButton.exists, "Cancel button should exist")
|
||||
cancelButton.tap()
|
||||
sleep(2)
|
||||
|
||||
// Should be back on contractors list
|
||||
let contractorsTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Contractors'")).firstMatch
|
||||
XCTAssertTrue(contractorsTab.exists, "Should be back on contractors list")
|
||||
|
||||
// Contractor should not exist
|
||||
let contractor = findContractor(name: "This will be canceled")
|
||||
XCTAssertFalse(contractor.exists, "Canceled contractor should not exist")
|
||||
}
|
||||
|
||||
// MARK: - 2. Basic Contractor Creation Tests
|
||||
|
||||
func test03_createContractorWithMinimalData() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let contractorName = "John Doe \(timestamp)"
|
||||
|
||||
let success = createContractor(name: contractorName)
|
||||
XCTAssertTrue(success, "Should successfully create contractor with minimal data")
|
||||
|
||||
let contractorInList = findContractor(name: contractorName)
|
||||
XCTAssertTrue(contractorInList.waitForExistence(timeout: 10), "Contractor should appear in list")
|
||||
}
|
||||
|
||||
func test04_createContractorWithAllFields() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let contractorName = "Jane Smith \(timestamp)"
|
||||
|
||||
let success = createContractor(
|
||||
name: contractorName,
|
||||
phone: "555-987-6543",
|
||||
email: "jane.smith@example.com",
|
||||
company: "Smith Plumbing Inc",
|
||||
specialty: "Plumbing"
|
||||
)
|
||||
XCTAssertTrue(success, "Should successfully create contractor with all fields")
|
||||
|
||||
let contractorInList = findContractor(name: contractorName)
|
||||
XCTAssertTrue(contractorInList.waitForExistence(timeout: 10), "Complete contractor should appear in list")
|
||||
}
|
||||
|
||||
func test05_createContractorWithDifferentSpecialties() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let specialties = ["Plumbing", "Electrical", "HVAC"]
|
||||
|
||||
for (index, specialty) in specialties.enumerated() {
|
||||
let contractorName = "\(specialty) Expert \(timestamp)_\(index)"
|
||||
let success = createContractor(name: contractorName, specialty: specialty)
|
||||
XCTAssertTrue(success, "Should create \(specialty) contractor")
|
||||
|
||||
navigateToContractorsTab()
|
||||
sleep(2)
|
||||
}
|
||||
|
||||
// Verify all contractors exist
|
||||
for (index, specialty) in specialties.enumerated() {
|
||||
let contractorName = "\(specialty) Expert \(timestamp)_\(index)"
|
||||
let contractor = findContractor(name: contractorName)
|
||||
XCTAssertTrue(contractor.exists, "\(specialty) contractor should exist in list")
|
||||
}
|
||||
}
|
||||
|
||||
func test06_createMultipleContractorsInSequence() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
|
||||
for i in 1...3 {
|
||||
let contractorName = "Sequential Contractor \(i) - \(timestamp)"
|
||||
let success = createContractor(name: contractorName)
|
||||
XCTAssertTrue(success, "Should create contractor \(i)")
|
||||
|
||||
navigateToContractorsTab()
|
||||
sleep(2)
|
||||
}
|
||||
|
||||
// Verify all contractors exist
|
||||
for i in 1...3 {
|
||||
let contractorName = "Sequential Contractor \(i) - \(timestamp)"
|
||||
let contractor = findContractor(name: contractorName)
|
||||
XCTAssertTrue(contractor.exists, "Contractor \(i) should exist in list")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 3. Edge Case Tests - Phone Numbers
|
||||
|
||||
func test07_createContractorWithDifferentPhoneFormats() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let phoneFormats = [
|
||||
("555-123-4567", "Dashed"),
|
||||
("(555) 123-4567", "Parentheses"),
|
||||
("5551234567", "NoFormat"),
|
||||
("555.123.4567", "Dotted")
|
||||
]
|
||||
|
||||
for (index, (phone, format)) in phoneFormats.enumerated() {
|
||||
let contractorName = "\(format) Phone \(timestamp)_\(index)"
|
||||
let success = createContractor(name: contractorName, phone: phone)
|
||||
XCTAssertTrue(success, "Should create contractor with \(format) phone format")
|
||||
|
||||
navigateToContractorsTab()
|
||||
sleep(2)
|
||||
}
|
||||
|
||||
// Verify all contractors exist
|
||||
for (index, (_, format)) in phoneFormats.enumerated() {
|
||||
let contractorName = "\(format) Phone \(timestamp)_\(index)"
|
||||
let contractor = findContractor(name: contractorName)
|
||||
XCTAssertTrue(contractor.exists, "Contractor with \(format) phone should exist")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 4. Edge Case Tests - Emails
|
||||
|
||||
func test08_createContractorWithValidEmails() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let emails = [
|
||||
"simple@example.com",
|
||||
"firstname.lastname@example.com",
|
||||
"email+tag@example.co.uk",
|
||||
"email_with_underscore@example.com"
|
||||
]
|
||||
|
||||
for (index, email) in emails.enumerated() {
|
||||
let contractorName = "Email Test \(index) - \(timestamp)"
|
||||
let success = createContractor(name: contractorName, email: email)
|
||||
XCTAssertTrue(success, "Should create contractor with email: \(email)")
|
||||
|
||||
navigateToContractorsTab()
|
||||
sleep(2)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 5. Edge Case Tests - Names
|
||||
|
||||
func test09_createContractorWithVeryLongName() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let longName = "John Christopher Alexander Montgomery Wellington III Esquire \(timestamp)"
|
||||
|
||||
let success = createContractor(name: longName)
|
||||
XCTAssertTrue(success, "Should handle very long names")
|
||||
|
||||
// Verify it appears (may be truncated in display)
|
||||
let contractor = findContractor(name: "John Christopher")
|
||||
XCTAssertTrue(contractor.waitForExistence(timeout: 10), "Long name contractor should exist")
|
||||
}
|
||||
|
||||
func test10_createContractorWithSpecialCharactersInName() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let specialName = "O'Brien-Smith Jr. \(timestamp)"
|
||||
|
||||
let success = createContractor(name: specialName)
|
||||
XCTAssertTrue(success, "Should handle special characters in names")
|
||||
|
||||
let contractor = findContractor(name: "O'Brien")
|
||||
XCTAssertTrue(contractor.waitForExistence(timeout: 10), "Contractor with special chars should exist")
|
||||
}
|
||||
|
||||
func test11_createContractorWithInternationalCharacters() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let internationalName = "José García \(timestamp)"
|
||||
|
||||
let success = createContractor(name: internationalName)
|
||||
XCTAssertTrue(success, "Should handle international characters")
|
||||
|
||||
let contractor = findContractor(name: "José")
|
||||
XCTAssertTrue(contractor.waitForExistence(timeout: 10), "Contractor with international chars should exist")
|
||||
}
|
||||
|
||||
func test12_createContractorWithEmojisInName() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let emojiName = "Bob 🔧 Builder \(timestamp)"
|
||||
|
||||
let success = createContractor(name: emojiName)
|
||||
XCTAssertTrue(success, "Should handle emojis in names")
|
||||
|
||||
let contractor = findContractor(name: "Bob")
|
||||
XCTAssertTrue(contractor.waitForExistence(timeout: 10), "Contractor with emojis should exist")
|
||||
}
|
||||
|
||||
// MARK: - 6. Contractor Editing Tests
|
||||
|
||||
func test13_editContractorName() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let originalName = "Original Contractor \(timestamp)"
|
||||
let newName = "Edited Contractor \(timestamp)"
|
||||
|
||||
// Create contractor
|
||||
guard createContractor(name: originalName) else {
|
||||
XCTFail("Failed to create contractor")
|
||||
return
|
||||
}
|
||||
|
||||
navigateToContractorsTab()
|
||||
sleep(2)
|
||||
|
||||
// Find and tap contractor
|
||||
let contractor = findContractor(name: originalName)
|
||||
XCTAssertTrue(contractor.waitForExistence(timeout: 5), "Contractor should exist")
|
||||
contractor.tap()
|
||||
sleep(2)
|
||||
|
||||
// Tap edit button (may be in menu)
|
||||
app/*@START_MENU_TOKEN@*/.images["ellipsis.circle"]/*[[".buttons[\"More\"].images",".buttons",".images[\"More\"]",".images[\"ellipsis.circle\"]"],[[[-1,3],[-1,2],[-1,1,1],[-1,0]],[[-1,3],[-1,2]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap()
|
||||
app/*@START_MENU_TOKEN@*/.buttons["pencil"]/*[[".buttons.containing(.image, identifier: \"pencil\")",".cells",".buttons[\"Edit\"]",".buttons[\"pencil\"]"],[[[-1,3],[-1,2],[-1,1,1],[-1,0]],[[-1,3],[-1,2]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap()
|
||||
|
||||
// Edit name
|
||||
let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Name'")).firstMatch
|
||||
if nameField.exists {
|
||||
nameField.tap()
|
||||
sleep(1)
|
||||
nameField.tap()
|
||||
sleep(1)
|
||||
app.menuItems["Select All"].tap()
|
||||
sleep(1)
|
||||
nameField.typeText(newName)
|
||||
|
||||
// Save (when editing, button should say "Save")
|
||||
let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch
|
||||
if saveButton.exists {
|
||||
saveButton.tap()
|
||||
sleep(3)
|
||||
|
||||
// Track new name
|
||||
createdContractorNames.append(newName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func test14_updateAllContractorFields() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let originalName = "Update All Fields \(timestamp)"
|
||||
let newName = "All Fields Updated \(timestamp)"
|
||||
let newPhone = "999-888-7777"
|
||||
let newEmail = "updated@contractor.com"
|
||||
let newCompany = "Updated Company LLC"
|
||||
|
||||
// Create contractor with initial values
|
||||
guard createContractor(
|
||||
name: originalName,
|
||||
phone: "555-123-4567",
|
||||
email: "original@contractor.com",
|
||||
company: "Original Company"
|
||||
) else {
|
||||
XCTFail("Failed to create contractor")
|
||||
return
|
||||
}
|
||||
|
||||
navigateToContractorsTab()
|
||||
sleep(2)
|
||||
|
||||
// Find and tap contractor
|
||||
let contractor = findContractor(name: originalName)
|
||||
XCTAssertTrue(contractor.waitForExistence(timeout: 5), "Contractor should exist")
|
||||
contractor.tap()
|
||||
sleep(2)
|
||||
|
||||
// Tap edit button (may be in menu)
|
||||
app/*@START_MENU_TOKEN@*/.images["ellipsis.circle"]/*[[".buttons[\"More\"].images",".buttons",".images[\"More\"]",".images[\"ellipsis.circle\"]"],[[[-1,3],[-1,2],[-1,1,1],[-1,0]],[[-1,3],[-1,2]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap()
|
||||
app/*@START_MENU_TOKEN@*/.buttons["pencil"]/*[[".buttons.containing(.image, identifier: \"pencil\")",".cells",".buttons[\"Edit\"]",".buttons[\"pencil\"]"],[[[-1,3],[-1,2],[-1,1,1],[-1,0]],[[-1,3],[-1,2]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap()
|
||||
|
||||
// Update name
|
||||
let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Name'")).firstMatch
|
||||
XCTAssertTrue(nameField.exists, "Name field should exist")
|
||||
nameField.tap()
|
||||
sleep(1)
|
||||
nameField.tap()
|
||||
sleep(1)
|
||||
app.menuItems["Select All"].tap()
|
||||
sleep(1)
|
||||
nameField.typeText(newName)
|
||||
|
||||
// Update phone
|
||||
let phoneField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Phone'")).firstMatch
|
||||
if phoneField.exists {
|
||||
phoneField.tap()
|
||||
sleep(1)
|
||||
phoneField.tap()
|
||||
sleep(1)
|
||||
app.menuItems["Select All"].tap()
|
||||
phoneField.typeText(newPhone)
|
||||
}
|
||||
|
||||
// Update email
|
||||
let emailField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Email'")).firstMatch
|
||||
if emailField.exists {
|
||||
emailField.tap()
|
||||
sleep(1)
|
||||
emailField.tap()
|
||||
sleep(1)
|
||||
app.menuItems["Select All"].tap()
|
||||
emailField.typeText(newEmail)
|
||||
}
|
||||
|
||||
// Update company
|
||||
let companyField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Company'")).firstMatch
|
||||
if companyField.exists {
|
||||
companyField.tap()
|
||||
sleep(1)
|
||||
companyField.tap()
|
||||
sleep(1)
|
||||
app.menuItems["Select All"].tap()
|
||||
companyField.typeText(newCompany)
|
||||
}
|
||||
|
||||
// Update specialty (if picker exists)
|
||||
let specialtyPicker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Specialty'")).firstMatch
|
||||
if specialtyPicker.exists {
|
||||
specialtyPicker.tap()
|
||||
sleep(1)
|
||||
// Select HVAC
|
||||
let hvacOption = app.buttons["HVAC"]
|
||||
if hvacOption.exists {
|
||||
hvacOption.tap()
|
||||
sleep(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Save (when editing, button should say "Save")
|
||||
let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch
|
||||
XCTAssertTrue(saveButton.exists, "Save button should exist when editing contractor")
|
||||
saveButton.tap()
|
||||
sleep(4)
|
||||
|
||||
// Track new name
|
||||
createdContractorNames.append(newName)
|
||||
|
||||
// Verify updated contractor appears in list with new name
|
||||
navigateToContractorsTab()
|
||||
sleep(2)
|
||||
let updatedContractor = findContractor(name: newName)
|
||||
XCTAssertTrue(updatedContractor.exists, "Contractor should show updated name in list")
|
||||
|
||||
// Tap on contractor to verify details were updated
|
||||
updatedContractor.tap()
|
||||
sleep(2)
|
||||
|
||||
// Verify updated phone appears in detail view
|
||||
let phoneText = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(newPhone)' OR label CONTAINS '999-888-7777' OR label CONTAINS '9998887777'")).firstMatch
|
||||
XCTAssertTrue(phoneText.exists, "Updated phone should be visible in detail view")
|
||||
|
||||
// Verify updated email appears in detail view
|
||||
let emailText = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(newEmail)'")).firstMatch
|
||||
XCTAssertTrue(emailText.exists, "Updated email should be visible in detail view")
|
||||
|
||||
// Verify updated company appears in detail view
|
||||
let companyText = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(newCompany)'")).firstMatch
|
||||
XCTAssertTrue(companyText.exists, "Updated company should be visible in detail view")
|
||||
|
||||
// Verify updated specialty (HVAC) appears
|
||||
let hvacBadge = app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'HVAC'")).firstMatch
|
||||
XCTAssertTrue(hvacBadge.exists || true, "Updated specialty should be visible (if shown in detail)")
|
||||
}
|
||||
|
||||
// MARK: - 7. Navigation & List Tests
|
||||
|
||||
func test15_navigateFromContractorsToOtherTabs() {
|
||||
// From Contractors tab
|
||||
navigateToContractorsTab()
|
||||
|
||||
// Navigate to Residences
|
||||
let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
|
||||
XCTAssertTrue(residencesTab.exists, "Residences tab should exist")
|
||||
residencesTab.tap()
|
||||
sleep(1)
|
||||
XCTAssertTrue(residencesTab.isSelected, "Should be on Residences tab")
|
||||
|
||||
// Navigate back to Contractors
|
||||
let contractorsTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Contractors'")).firstMatch
|
||||
contractorsTab.tap()
|
||||
sleep(1)
|
||||
XCTAssertTrue(contractorsTab.isSelected, "Should be back on Contractors tab")
|
||||
|
||||
// Navigate to Tasks
|
||||
let tasksTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch
|
||||
XCTAssertTrue(tasksTab.exists, "Tasks tab should exist")
|
||||
tasksTab.tap()
|
||||
sleep(1)
|
||||
XCTAssertTrue(tasksTab.isSelected, "Should be on Tasks tab")
|
||||
|
||||
// Back to Contractors
|
||||
contractorsTab.tap()
|
||||
sleep(1)
|
||||
XCTAssertTrue(contractorsTab.isSelected, "Should be back on Contractors tab again")
|
||||
}
|
||||
|
||||
func test16_refreshContractorsList() {
|
||||
navigateToContractorsTab()
|
||||
sleep(2)
|
||||
|
||||
// Pull to refresh (if implemented) or use refresh button
|
||||
let refreshButton = app.navigationBars.buttons.containing(NSPredicate(format: "label CONTAINS 'arrow.clockwise' OR label CONTAINS 'refresh'")).firstMatch
|
||||
if refreshButton.exists {
|
||||
refreshButton.tap()
|
||||
sleep(3)
|
||||
}
|
||||
|
||||
// Verify we're still on contractors tab
|
||||
let contractorsTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Contractors'")).firstMatch
|
||||
XCTAssertTrue(contractorsTab.isSelected, "Should still be on Contractors tab after refresh")
|
||||
}
|
||||
|
||||
func test17_viewContractorDetails() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let contractorName = "Detail View Test \(timestamp)"
|
||||
|
||||
// Create contractor
|
||||
guard createContractor(name: contractorName, email: "test@example.com", company: "Test Company") else {
|
||||
XCTFail("Failed to create contractor")
|
||||
return
|
||||
}
|
||||
|
||||
navigateToContractorsTab()
|
||||
sleep(2)
|
||||
|
||||
// Tap on contractor
|
||||
let contractor = findContractor(name: contractorName)
|
||||
XCTAssertTrue(contractor.exists, "Contractor should exist")
|
||||
contractor.tap()
|
||||
sleep(3)
|
||||
|
||||
// Verify detail view appears with contact info
|
||||
let phoneLabel = app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'Phone' OR label CONTAINS '555'")).firstMatch
|
||||
let emailLabel = app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'Email' OR label CONTAINS 'test@example.com'")).firstMatch
|
||||
|
||||
XCTAssertTrue(phoneLabel.exists || emailLabel.exists, "Detail view should show contact information")
|
||||
}
|
||||
|
||||
// MARK: - 8. Data Persistence Tests
|
||||
|
||||
func test18_contractorPersistsAfterBackgroundingApp() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let contractorName = "Persistence Test \(timestamp)"
|
||||
|
||||
// Create contractor
|
||||
guard createContractor(name: contractorName) else {
|
||||
XCTFail("Failed to create contractor")
|
||||
return
|
||||
}
|
||||
|
||||
navigateToContractorsTab()
|
||||
sleep(2)
|
||||
|
||||
// Verify contractor exists
|
||||
var contractor = findContractor(name: contractorName)
|
||||
XCTAssertTrue(contractor.exists, "Contractor should exist before backgrounding")
|
||||
|
||||
// Background and reactivate app
|
||||
XCUIDevice.shared.press(.home)
|
||||
sleep(2)
|
||||
app.activate()
|
||||
sleep(3)
|
||||
|
||||
// Navigate back to contractors
|
||||
navigateToContractorsTab()
|
||||
sleep(2)
|
||||
|
||||
// Verify contractor still exists
|
||||
contractor = findContractor(name: contractorName)
|
||||
XCTAssertTrue(contractor.exists, "Contractor should persist after backgrounding app")
|
||||
}
|
||||
|
||||
// MARK: - 9. Performance Tests
|
||||
|
||||
func test19_contractorListPerformance() {
|
||||
measure(metrics: [XCTClockMetric(), XCTMemoryMetric()]) {
|
||||
navigateToContractorsTab()
|
||||
sleep(2)
|
||||
}
|
||||
}
|
||||
|
||||
func test20_contractorCreationPerformance() {
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
|
||||
measure(metrics: [XCTClockMetric()]) {
|
||||
let contractorName = "Performance Test \(timestamp)_\(UUID().uuidString.prefix(8))"
|
||||
_ = createContractor(name: contractorName)
|
||||
}
|
||||
}
|
||||
}
|
||||
944
iosApp/CaseraUITests/Suite8_DocumentWarrantyTests.swift
Normal file
944
iosApp/CaseraUITests/Suite8_DocumentWarrantyTests.swift
Normal file
@@ -0,0 +1,944 @@
|
||||
import XCTest
|
||||
|
||||
/// Comprehensive documents and warranties testing suite covering all scenarios, edge cases, and variations
|
||||
/// Tests both document types (permits, receipts, etc.) and warranties with filtering, searching, and CRUD operations
|
||||
final class Suite8_DocumentWarrantyTests: BaseUITestCase {
|
||||
override var includeResetStateLaunchArgument: Bool { false }
|
||||
|
||||
|
||||
// Test data tracking
|
||||
var createdDocumentTitles: [String] = []
|
||||
var currentResidenceId: Int32?
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
|
||||
// Ensure user is logged in
|
||||
UITestHelpers.ensureLoggedIn(app: app)
|
||||
|
||||
// Navigate to a residence first (documents are residence-specific)
|
||||
navigateToFirstResidence()
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
createdDocumentTitles.removeAll()
|
||||
currentResidenceId = nil
|
||||
try super.tearDownWithError()
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
private func navigateToFirstResidence() {
|
||||
// Tap Residences tab
|
||||
let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
|
||||
if residencesTab.waitForExistence(timeout: 5) {
|
||||
residencesTab.tap()
|
||||
sleep(3)
|
||||
}
|
||||
|
||||
// Tap first residence card
|
||||
let firstResidence = app.collectionViews.cells.firstMatch
|
||||
if firstResidence.waitForExistence(timeout: 5) {
|
||||
firstResidence.tap()
|
||||
sleep(2)
|
||||
}
|
||||
}
|
||||
|
||||
private func navigateToDocumentsTab() {
|
||||
// Look for Documents tab or navigation link
|
||||
let documentsButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Documents' OR label CONTAINS[c] 'Warranties'")).firstMatch
|
||||
if documentsButton.waitForExistence(timeout: 5) {
|
||||
documentsButton.tap()
|
||||
sleep(3)
|
||||
}
|
||||
}
|
||||
|
||||
private func openDocumentForm() -> Bool {
|
||||
let addButton = findAddButton()
|
||||
guard addButton.exists && addButton.isEnabled else { return false }
|
||||
addButton.tap()
|
||||
sleep(3)
|
||||
|
||||
// Verify form opened
|
||||
let titleField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Title'")).firstMatch
|
||||
return titleField.waitForExistence(timeout: 5)
|
||||
}
|
||||
|
||||
private func findAddButton() -> XCUIElement {
|
||||
sleep(2)
|
||||
|
||||
// Look for add button by various methods
|
||||
let navBarButtons = app.navigationBars.buttons
|
||||
for i in 0..<navBarButtons.count {
|
||||
let button = navBarButtons.element(boundBy: i)
|
||||
if button.label == "plus" || button.label.contains("Add") {
|
||||
if button.isEnabled {
|
||||
return button
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: look for any button with plus icon
|
||||
return app.buttons.containing(NSPredicate(format: "label CONTAINS 'plus'")).firstMatch
|
||||
}
|
||||
|
||||
private func fillTextField(placeholder: String, text: String) {
|
||||
let field = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] '\(placeholder)'")).firstMatch
|
||||
if field.exists {
|
||||
field.tap()
|
||||
field.typeText(text)
|
||||
}
|
||||
}
|
||||
|
||||
private func fillTextEditor(text: String) {
|
||||
let textEditor = app.textViews.firstMatch
|
||||
if textEditor.exists {
|
||||
textEditor.tap()
|
||||
textEditor.typeText(text)
|
||||
}
|
||||
}
|
||||
|
||||
private func selectProperty() {
|
||||
// Open the picker
|
||||
app.buttons["Select Property, Select Property"].tap()
|
||||
|
||||
// Try cells first (common for Picker list)
|
||||
let secondCell = app.cells.element(boundBy: 1)
|
||||
if secondCell.waitForExistence(timeout: 5) {
|
||||
secondCell.tap()
|
||||
} else {
|
||||
// Fallback: second static text after the title
|
||||
let allTexts = app.staticTexts.allElementsBoundByIndex
|
||||
// Expect something like: [ "Select Property" (title), "Select Property", "Test Home for Comprehensive Tasks", ... ]
|
||||
// So the second item row label is usually at index 2
|
||||
let secondItemText = allTexts[2]
|
||||
secondItemText.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func selectDocumentType(type: String) {
|
||||
let typePicker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Type'")).firstMatch
|
||||
if typePicker.exists {
|
||||
typePicker.tap()
|
||||
sleep(1)
|
||||
|
||||
let typeButton = app.buttons[type]
|
||||
if typeButton.exists {
|
||||
typeButton.tap()
|
||||
sleep(1)
|
||||
} else {
|
||||
// Try cells if it's a navigation style picker
|
||||
let cells = app.cells
|
||||
for i in 0..<cells.count {
|
||||
let cell = cells.element(boundBy: i)
|
||||
if cell.staticTexts[type].exists {
|
||||
cell.tap()
|
||||
sleep(1)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func selectCategory(category: String) {
|
||||
let categoryPicker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Category'")).firstMatch
|
||||
if categoryPicker.exists {
|
||||
categoryPicker.tap()
|
||||
sleep(1)
|
||||
|
||||
let categoryButton = app.buttons[category]
|
||||
if categoryButton.exists {
|
||||
categoryButton.tap()
|
||||
sleep(1)
|
||||
} else {
|
||||
let cells = app.cells
|
||||
for i in 0..<cells.count {
|
||||
let cell = cells.element(boundBy: i)
|
||||
if cell.staticTexts[category].exists {
|
||||
cell.tap()
|
||||
sleep(1)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func selectDate(dateType: String, daysFromNow: Int) {
|
||||
let datePicker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] '\(dateType)'")).firstMatch
|
||||
if datePicker.exists {
|
||||
datePicker.tap()
|
||||
sleep(1)
|
||||
|
||||
// Look for date picker and set date
|
||||
let datePickerWheel = app.datePickers.firstMatch
|
||||
if datePickerWheel.exists {
|
||||
let calendar = Calendar.current
|
||||
let targetDate = calendar.date(byAdding: .day, value: daysFromNow, to: Date())!
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "MMM d, yyyy"
|
||||
let dateString = formatter.string(from: targetDate)
|
||||
|
||||
// Try to type the date or interact with picker
|
||||
sleep(1)
|
||||
}
|
||||
|
||||
// Dismiss picker
|
||||
app.buttons["Done"].tap()
|
||||
sleep(1)
|
||||
}
|
||||
}
|
||||
|
||||
private func submitForm() -> Bool {
|
||||
let submitButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save' OR label CONTAINS[c] 'Add' OR label CONTAINS[c] 'Create'")).firstMatch
|
||||
guard submitButton.exists && submitButton.isEnabled else { return false }
|
||||
submitButton.tap()
|
||||
sleep(3)
|
||||
return true
|
||||
}
|
||||
|
||||
private func cancelForm() {
|
||||
let cancelButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Cancel'")).firstMatch
|
||||
if cancelButton.exists {
|
||||
cancelButton.tap()
|
||||
sleep(2)
|
||||
}
|
||||
}
|
||||
|
||||
private func switchToWarrantiesTab() {
|
||||
app/*@START_MENU_TOKEN@*/.buttons["checkmark.shield"]/*[[".segmentedControls",".buttons[\"Warranties\"]",".buttons[\"checkmark.shield\"]"],[[[-1,2],[-1,1],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap()
|
||||
}
|
||||
|
||||
private func switchToDocumentsTab() {
|
||||
app/*@START_MENU_TOKEN@*/.buttons["doc.text"]/*[[".segmentedControls",".buttons[\"Documents\"]",".buttons[\"doc.text\"]"],[[[-1,2],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap()
|
||||
}
|
||||
|
||||
private func searchFor(text: String) {
|
||||
let searchField = app.searchFields.firstMatch
|
||||
if searchField.exists {
|
||||
searchField.tap()
|
||||
searchField.typeText(text)
|
||||
sleep(2)
|
||||
}
|
||||
}
|
||||
|
||||
private func clearSearch() {
|
||||
let searchField = app.searchFields.firstMatch
|
||||
if searchField.exists {
|
||||
let clearButton = searchField.buttons["Clear text"]
|
||||
if clearButton.exists {
|
||||
clearButton.tap()
|
||||
sleep(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func applyFilter(filterName: String) {
|
||||
// Open filter menu
|
||||
let filterButton = app.buttons.containing(NSPredicate(format: "label CONTAINS 'line.3.horizontal.decrease'")).firstMatch
|
||||
if filterButton.exists {
|
||||
filterButton.tap()
|
||||
sleep(1)
|
||||
|
||||
// Select filter option
|
||||
let filterOption = app.buttons[filterName]
|
||||
if filterOption.exists {
|
||||
filterOption.tap()
|
||||
sleep(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func toggleActiveFilter() {
|
||||
let activeFilterButton = app.buttons.containing(NSPredicate(format: "label CONTAINS 'checkmark.circle'")).firstMatch
|
||||
if activeFilterButton.exists {
|
||||
activeFilterButton.tap()
|
||||
sleep(2)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Test Cases
|
||||
|
||||
// MARK: Navigation Tests
|
||||
|
||||
func test01_NavigateToDocumentsScreen() {
|
||||
navigateToDocumentsTab()
|
||||
|
||||
// Verify we're on documents screen
|
||||
let navigationTitle = app.navigationBars["Documents & Warranties"]
|
||||
XCTAssertTrue(navigationTitle.waitForExistence(timeout: 5), "Should navigate to Documents & Warranties screen")
|
||||
|
||||
// Verify tabs are visible
|
||||
let warrantiesTab = app.buttons["Warranties"]
|
||||
let documentsTab = app.buttons["Documents"]
|
||||
XCTAssertTrue(warrantiesTab.exists || documentsTab.exists, "Should see tab switcher")
|
||||
}
|
||||
|
||||
func test02_SwitchBetweenWarrantiesAndDocuments() {
|
||||
navigateToDocumentsTab()
|
||||
|
||||
// Start on warranties tab
|
||||
switchToWarrantiesTab()
|
||||
sleep(1)
|
||||
|
||||
// Switch to documents tab
|
||||
switchToDocumentsTab()
|
||||
sleep(1)
|
||||
|
||||
// Switch back to warranties
|
||||
switchToWarrantiesTab()
|
||||
sleep(1)
|
||||
|
||||
// Should not crash and tabs should still exist
|
||||
let warrantiesTab = app.buttons["Warranties"]
|
||||
XCTAssertTrue(warrantiesTab.exists, "Tabs should remain functional after switching")
|
||||
}
|
||||
|
||||
// MARK: Document Creation Tests
|
||||
|
||||
func test03_CreateDocumentWithAllFields() {
|
||||
navigateToDocumentsTab()
|
||||
switchToDocumentsTab()
|
||||
|
||||
XCTAssertTrue(openDocumentForm(), "Should open document form")
|
||||
|
||||
let testTitle = "Test Permit \(UUID().uuidString.prefix(8))"
|
||||
createdDocumentTitles.append(testTitle)
|
||||
|
||||
// Fill all fields
|
||||
selectProperty() // REQUIRED - Select property first
|
||||
fillTextField(placeholder: "Title", text: testTitle)
|
||||
selectDocumentType(type: "Insurance")
|
||||
fillTextEditor(text: "Test permit description with detailed information")
|
||||
fillTextField(placeholder: "Tags", text: "construction,permit")
|
||||
fillTextField(placeholder: "Item Name", text: "Kitchen Renovation")
|
||||
fillTextField(placeholder: "Location", text: "Main Kitchen")
|
||||
|
||||
XCTAssertTrue(submitForm(), "Should submit form successfully")
|
||||
|
||||
// Verify document appears in list
|
||||
sleep(2)
|
||||
let documentCard = app.staticTexts[testTitle]
|
||||
XCTAssertTrue(documentCard.exists, "Created document should appear in list")
|
||||
}
|
||||
|
||||
func test04_CreateDocumentWithMinimalFields() {
|
||||
navigateToDocumentsTab()
|
||||
switchToDocumentsTab()
|
||||
|
||||
XCTAssertTrue(openDocumentForm(), "Should open document form")
|
||||
|
||||
let testTitle = "Min Doc \(UUID().uuidString.prefix(8))"
|
||||
createdDocumentTitles.append(testTitle)
|
||||
|
||||
// Fill only required fields
|
||||
selectProperty() // REQUIRED - Select property first
|
||||
fillTextField(placeholder: "Title", text: testTitle)
|
||||
selectDocumentType(type: "Insurance")
|
||||
|
||||
XCTAssertTrue(submitForm(), "Should submit form with minimal fields")
|
||||
|
||||
// Verify document appears
|
||||
sleep(2)
|
||||
let documentCard = app.staticTexts[testTitle]
|
||||
XCTAssertTrue(documentCard.exists, "Document with minimal fields should appear")
|
||||
}
|
||||
|
||||
func test05_CreateDocumentWithEmptyTitle_ShouldFail() {
|
||||
navigateToDocumentsTab()
|
||||
switchToDocumentsTab()
|
||||
|
||||
XCTAssertTrue(openDocumentForm(), "Should open document form")
|
||||
|
||||
// Try to submit without title
|
||||
selectProperty() // REQUIRED - Select property first
|
||||
selectDocumentType(type: "Insurance")
|
||||
|
||||
let submitButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save' OR label CONTAINS[c] 'Add'")).firstMatch
|
||||
|
||||
// Submit button should be disabled or show error
|
||||
if submitButton.exists && submitButton.isEnabled {
|
||||
submitButton.tap()
|
||||
sleep(2)
|
||||
|
||||
// Should show error message
|
||||
let errorMessage = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'required' OR label CONTAINS[c] 'title'")).firstMatch
|
||||
XCTAssertTrue(errorMessage.exists, "Should show validation error for missing title")
|
||||
}
|
||||
|
||||
cancelForm()
|
||||
}
|
||||
|
||||
// MARK: Warranty Creation Tests
|
||||
|
||||
func test06_CreateWarrantyWithAllFields() {
|
||||
navigateToDocumentsTab()
|
||||
switchToWarrantiesTab()
|
||||
|
||||
XCTAssertTrue(openDocumentForm(), "Should open warranty form")
|
||||
|
||||
let testTitle = "Test Warranty \(UUID().uuidString.prefix(8))"
|
||||
createdDocumentTitles.append(testTitle)
|
||||
|
||||
// Fill all warranty fields (including required fields)
|
||||
selectProperty() // REQUIRED - Select property first
|
||||
fillTextField(placeholder: "Title", text: testTitle)
|
||||
selectCategory(category: "Appliances")
|
||||
fillTextField(placeholder: "Item Name", text: "Dishwasher") // REQUIRED
|
||||
fillTextField(placeholder: "Provider", text: "Bosch") // REQUIRED
|
||||
fillTextField(placeholder: "Model", text: "SHPM65Z55N")
|
||||
fillTextField(placeholder: "Serial", text: "SN123456789")
|
||||
fillTextField(placeholder: "Provider Contact", text: "1-800-BOSCH-00")
|
||||
fillTextEditor(text: "Full warranty coverage for 2 years")
|
||||
|
||||
// Select dates
|
||||
selectDate(dateType: "Start Date", daysFromNow: -30)
|
||||
selectDate(dateType: "End Date", daysFromNow: 700) // ~2 years
|
||||
|
||||
XCTAssertTrue(submitForm(), "Should submit warranty successfully")
|
||||
|
||||
// Verify warranty appears
|
||||
sleep(2)
|
||||
let warrantyCard = app.staticTexts[testTitle]
|
||||
XCTAssertTrue(warrantyCard.exists, "Created warranty should appear in list")
|
||||
}
|
||||
|
||||
func test07_CreateWarrantyWithFutureDates() {
|
||||
navigateToDocumentsTab()
|
||||
switchToWarrantiesTab()
|
||||
|
||||
XCTAssertTrue(openDocumentForm(), "Should open warranty form")
|
||||
|
||||
let testTitle = "Future Warranty \(UUID().uuidString.prefix(8))"
|
||||
createdDocumentTitles.append(testTitle)
|
||||
|
||||
selectProperty() // REQUIRED - Select property first
|
||||
fillTextField(placeholder: "Title", text: testTitle)
|
||||
selectCategory(category: "HVAC")
|
||||
fillTextField(placeholder: "Item Name", text: "Air Conditioner") // REQUIRED
|
||||
fillTextField(placeholder: "Provider", text: "Carrier HVAC") // REQUIRED
|
||||
|
||||
// Set start date in future
|
||||
selectDate(dateType: "Start Date", daysFromNow: 30)
|
||||
selectDate(dateType: "End Date", daysFromNow: 400)
|
||||
|
||||
XCTAssertTrue(submitForm(), "Should create warranty with future dates")
|
||||
|
||||
sleep(2)
|
||||
let warrantyCard = app.staticTexts[testTitle]
|
||||
XCTAssertTrue(warrantyCard.exists, "Warranty with future dates should be created")
|
||||
}
|
||||
|
||||
func test08_CreateExpiredWarranty() {
|
||||
navigateToDocumentsTab()
|
||||
switchToWarrantiesTab()
|
||||
|
||||
XCTAssertTrue(openDocumentForm(), "Should open warranty form")
|
||||
|
||||
let testTitle = "Expired Warranty \(UUID().uuidString.prefix(8))"
|
||||
createdDocumentTitles.append(testTitle)
|
||||
|
||||
selectProperty() // REQUIRED - Select property first
|
||||
fillTextField(placeholder: "Title", text: testTitle)
|
||||
selectCategory(category: "Plumbing")
|
||||
fillTextField(placeholder: "Item Name", text: "Water Heater") // REQUIRED
|
||||
fillTextField(placeholder: "Provider", text: "AO Smith") // REQUIRED
|
||||
|
||||
// Set dates in the past
|
||||
selectDate(dateType: "Start Date", daysFromNow: -400)
|
||||
selectDate(dateType: "End Date", daysFromNow: -30)
|
||||
|
||||
XCTAssertTrue(submitForm(), "Should create expired warranty")
|
||||
|
||||
sleep(2)
|
||||
// Expired warranty might not show with active filter on
|
||||
// Toggle active filter off to see it
|
||||
toggleActiveFilter()
|
||||
sleep(1)
|
||||
|
||||
let warrantyCard = app.staticTexts[testTitle]
|
||||
XCTAssertTrue(warrantyCard.exists, "Expired warranty should be created and visible when filter is off")
|
||||
}
|
||||
|
||||
// MARK: Search and Filter Tests
|
||||
|
||||
func test09_SearchDocumentsByTitle() {
|
||||
navigateToDocumentsTab()
|
||||
switchToDocumentsTab()
|
||||
|
||||
// Create a test document first
|
||||
XCTAssertTrue(openDocumentForm(), "Should open form")
|
||||
let searchableTitle = "Searchable Doc \(UUID().uuidString.prefix(8))"
|
||||
createdDocumentTitles.append(searchableTitle)
|
||||
selectProperty() // REQUIRED - Select property first
|
||||
fillTextField(placeholder: "Title", text: searchableTitle)
|
||||
selectDocumentType(type: "Insurance")
|
||||
XCTAssertTrue(submitForm(), "Should create document")
|
||||
sleep(2)
|
||||
|
||||
// Search for it
|
||||
searchFor(text: String(searchableTitle.prefix(15)))
|
||||
|
||||
// Should find the document
|
||||
let foundDocument = app.staticTexts[searchableTitle]
|
||||
XCTAssertTrue(foundDocument.exists, "Should find document by search")
|
||||
|
||||
clearSearch()
|
||||
}
|
||||
|
||||
func test10_FilterWarrantiesByCategory() {
|
||||
navigateToDocumentsTab()
|
||||
switchToWarrantiesTab()
|
||||
|
||||
// Apply category filter
|
||||
applyFilter(filterName: "Appliances")
|
||||
|
||||
sleep(2)
|
||||
|
||||
// Should show filter chip or indication
|
||||
let filterChip = app.staticTexts["Appliances"]
|
||||
XCTAssertTrue(filterChip.exists || app.buttons["Appliances"].exists, "Should show active category filter")
|
||||
|
||||
// Clear filter
|
||||
applyFilter(filterName: "All Categories")
|
||||
}
|
||||
|
||||
func test11_FilterDocumentsByType() {
|
||||
navigateToDocumentsTab()
|
||||
switchToDocumentsTab()
|
||||
|
||||
// Apply type filter
|
||||
applyFilter(filterName: "Permit")
|
||||
|
||||
sleep(2)
|
||||
|
||||
// Should show filter indication
|
||||
let filterChip = app.staticTexts["Permit"]
|
||||
XCTAssertTrue(filterChip.exists || app.buttons["Permit"].exists, "Should show active type filter")
|
||||
|
||||
// Clear filter
|
||||
applyFilter(filterName: "All Types")
|
||||
}
|
||||
|
||||
func test12_ToggleActiveWarrantiesFilter() {
|
||||
navigateToDocumentsTab()
|
||||
switchToWarrantiesTab()
|
||||
|
||||
// Toggle active filter off
|
||||
toggleActiveFilter()
|
||||
sleep(1)
|
||||
|
||||
// Toggle it back on
|
||||
toggleActiveFilter()
|
||||
sleep(1)
|
||||
|
||||
// Should not crash
|
||||
let warrantiesTab = app.buttons["Warranties"]
|
||||
XCTAssertTrue(warrantiesTab.exists, "Active filter toggle should work without crashing")
|
||||
}
|
||||
|
||||
// MARK: Document Detail Tests
|
||||
|
||||
func test13_ViewDocumentDetail() {
|
||||
navigateToDocumentsTab()
|
||||
switchToDocumentsTab()
|
||||
|
||||
// Create a document
|
||||
XCTAssertTrue(openDocumentForm(), "Should open form")
|
||||
let testTitle = "Detail Test Doc \(UUID().uuidString.prefix(8))"
|
||||
createdDocumentTitles.append(testTitle)
|
||||
selectProperty() // REQUIRED - Select property first
|
||||
fillTextField(placeholder: "Title", text: testTitle)
|
||||
selectDocumentType(type: "Insurance")
|
||||
fillTextEditor(text: "This is a test receipt with details")
|
||||
XCTAssertTrue(submitForm(), "Should create document")
|
||||
sleep(2)
|
||||
|
||||
// Tap on the document card
|
||||
let documentCard = app.staticTexts[testTitle]
|
||||
XCTAssertTrue(documentCard.exists, "Document should exist in list")
|
||||
documentCard.tap()
|
||||
sleep(2)
|
||||
|
||||
// Should show detail screen
|
||||
let detailTitle = app.staticTexts[testTitle]
|
||||
XCTAssertTrue(detailTitle.exists, "Should show document detail screen")
|
||||
|
||||
// Go back
|
||||
let backButton = app.navigationBars.buttons.firstMatch
|
||||
backButton.tap()
|
||||
sleep(1)
|
||||
}
|
||||
|
||||
func test14_ViewWarrantyDetailWithDates() {
|
||||
navigateToDocumentsTab()
|
||||
switchToWarrantiesTab()
|
||||
|
||||
// Create a warranty
|
||||
XCTAssertTrue(openDocumentForm(), "Should open form")
|
||||
let testTitle = "Warranty Detail Test \(UUID().uuidString.prefix(8))"
|
||||
createdDocumentTitles.append(testTitle)
|
||||
selectProperty() // REQUIRED - Select property first
|
||||
fillTextField(placeholder: "Title", text: testTitle)
|
||||
selectCategory(category: "Appliances")
|
||||
fillTextField(placeholder: "Item Name", text: "Test Appliance") // REQUIRED
|
||||
fillTextField(placeholder: "Provider", text: "Test Company") // REQUIRED
|
||||
selectDate(dateType: "Start Date", daysFromNow: -30)
|
||||
selectDate(dateType: "End Date", daysFromNow: 335)
|
||||
XCTAssertTrue(submitForm(), "Should create warranty")
|
||||
sleep(2)
|
||||
|
||||
// Tap on warranty
|
||||
let warrantyCard = app.staticTexts[testTitle]
|
||||
XCTAssertTrue(warrantyCard.exists, "Warranty should exist")
|
||||
warrantyCard.tap()
|
||||
sleep(2)
|
||||
|
||||
// Should show warranty details with dates
|
||||
let detailScreen = app.staticTexts[testTitle]
|
||||
XCTAssertTrue(detailScreen.exists, "Should show warranty detail")
|
||||
|
||||
// Look for date information
|
||||
let dateLabels = app.staticTexts.matching(NSPredicate(format: "label CONTAINS[c] '20' OR label CONTAINS[c] 'Start' OR label CONTAINS[c] 'End'"))
|
||||
XCTAssertTrue(dateLabels.count > 0, "Should display date information")
|
||||
|
||||
// Go back
|
||||
app.navigationBars.buttons.firstMatch.tap()
|
||||
sleep(1)
|
||||
}
|
||||
|
||||
// MARK: Edit Tests
|
||||
|
||||
func test15_EditDocumentTitle() {
|
||||
navigateToDocumentsTab()
|
||||
switchToDocumentsTab()
|
||||
|
||||
// Create document
|
||||
XCTAssertTrue(openDocumentForm(), "Should open form")
|
||||
let originalTitle = "Edit Test \(UUID().uuidString.prefix(8))"
|
||||
createdDocumentTitles.append(originalTitle)
|
||||
selectProperty() // REQUIRED - Select property first
|
||||
fillTextField(placeholder: "Title", text: originalTitle)
|
||||
selectDocumentType(type: "Insurance")
|
||||
XCTAssertTrue(submitForm(), "Should create document")
|
||||
sleep(2)
|
||||
|
||||
// Open detail
|
||||
let documentCard = app.staticTexts[originalTitle]
|
||||
XCTAssertTrue(documentCard.exists, "Document should exist")
|
||||
documentCard.tap()
|
||||
sleep(2)
|
||||
|
||||
// Tap edit button
|
||||
let editButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Edit'")).firstMatch
|
||||
if editButton.exists {
|
||||
editButton.tap()
|
||||
sleep(2)
|
||||
|
||||
// Change title
|
||||
let titleField = app.textFields.containing(NSPredicate(format: "value == '\(originalTitle)'")).firstMatch
|
||||
if titleField.exists {
|
||||
titleField.tap()
|
||||
titleField.clearText()
|
||||
let newTitle = "Edited \(originalTitle)"
|
||||
titleField.typeText(newTitle)
|
||||
createdDocumentTitles.append(newTitle)
|
||||
|
||||
XCTAssertTrue(submitForm(), "Should save edited document")
|
||||
sleep(2)
|
||||
|
||||
// Verify new title appears
|
||||
let updatedTitle = app.staticTexts[newTitle]
|
||||
XCTAssertTrue(updatedTitle.exists, "Updated title should appear")
|
||||
}
|
||||
}
|
||||
|
||||
// Go back to list
|
||||
app.navigationBars.buttons.element(boundBy: 0).tap()
|
||||
sleep(1)
|
||||
}
|
||||
|
||||
func test16_EditWarrantyDates() {
|
||||
navigateToDocumentsTab()
|
||||
switchToWarrantiesTab()
|
||||
|
||||
// Create warranty
|
||||
XCTAssertTrue(openDocumentForm(), "Should open form")
|
||||
let testTitle = "Edit Dates Warranty \(UUID().uuidString.prefix(8))"
|
||||
createdDocumentTitles.append(testTitle)
|
||||
selectProperty() // REQUIRED - Select property first
|
||||
fillTextField(placeholder: "Title", text: testTitle)
|
||||
selectCategory(category: "Electronics")
|
||||
fillTextField(placeholder: "Item Name", text: "TV") // REQUIRED
|
||||
fillTextField(placeholder: "Provider", text: "Samsung") // REQUIRED
|
||||
selectDate(dateType: "Start Date", daysFromNow: -60)
|
||||
selectDate(dateType: "End Date", daysFromNow: 305)
|
||||
XCTAssertTrue(submitForm(), "Should create warranty")
|
||||
sleep(2)
|
||||
|
||||
// Open and edit
|
||||
let warrantyCard = app.staticTexts[testTitle]
|
||||
XCTAssertTrue(warrantyCard.exists, "Warranty should exist")
|
||||
warrantyCard.tap()
|
||||
sleep(2)
|
||||
|
||||
let editButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Edit'")).firstMatch
|
||||
if editButton.exists {
|
||||
editButton.tap()
|
||||
sleep(2)
|
||||
|
||||
// Change end date to extend warranty
|
||||
selectDate(dateType: "End Date", daysFromNow: 730) // 2 years
|
||||
|
||||
XCTAssertTrue(submitForm(), "Should save edited warranty dates")
|
||||
sleep(2)
|
||||
}
|
||||
|
||||
app.navigationBars.buttons.element(boundBy: 0).tap()
|
||||
sleep(1)
|
||||
}
|
||||
|
||||
// MARK: Delete Tests
|
||||
|
||||
func test17_DeleteDocument() {
|
||||
navigateToDocumentsTab()
|
||||
switchToDocumentsTab()
|
||||
|
||||
// Create document to delete
|
||||
XCTAssertTrue(openDocumentForm(), "Should open form")
|
||||
let deleteTitle = "To Delete \(UUID().uuidString.prefix(8))"
|
||||
selectProperty() // REQUIRED - Select property first
|
||||
fillTextField(placeholder: "Title", text: deleteTitle)
|
||||
selectDocumentType(type: "Insurance")
|
||||
XCTAssertTrue(submitForm(), "Should create document")
|
||||
sleep(2)
|
||||
|
||||
// Open detail
|
||||
let documentCard = app.staticTexts[deleteTitle]
|
||||
XCTAssertTrue(documentCard.exists, "Document should exist")
|
||||
documentCard.tap()
|
||||
sleep(2)
|
||||
|
||||
// Find and tap delete button
|
||||
let deleteButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Delete' OR label CONTAINS[c] 'trash'")).firstMatch
|
||||
if deleteButton.exists {
|
||||
deleteButton.tap()
|
||||
sleep(1)
|
||||
|
||||
// Confirm deletion
|
||||
let confirmButton = app.alerts.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Delete' OR label CONTAINS[c] 'Confirm'")).firstMatch
|
||||
if confirmButton.exists {
|
||||
confirmButton.tap()
|
||||
sleep(2)
|
||||
}
|
||||
|
||||
// Should navigate back to list
|
||||
sleep(2)
|
||||
|
||||
// Verify document no longer exists
|
||||
let deletedCard = app.staticTexts[deleteTitle]
|
||||
XCTAssertFalse(deletedCard.exists, "Deleted document should not appear in list")
|
||||
}
|
||||
}
|
||||
|
||||
func test18_DeleteWarranty() {
|
||||
navigateToDocumentsTab()
|
||||
switchToWarrantiesTab()
|
||||
|
||||
// Create warranty to delete
|
||||
XCTAssertTrue(openDocumentForm(), "Should open form")
|
||||
let deleteTitle = "Warranty to Delete \(UUID().uuidString.prefix(8))"
|
||||
selectProperty() // REQUIRED - Select property first
|
||||
fillTextField(placeholder: "Title", text: deleteTitle)
|
||||
selectCategory(category: "Other")
|
||||
fillTextField(placeholder: "Item Name", text: "Test Item") // REQUIRED
|
||||
fillTextField(placeholder: "Provider", text: "Test Provider") // REQUIRED
|
||||
XCTAssertTrue(submitForm(), "Should create warranty")
|
||||
sleep(2)
|
||||
|
||||
// Open and delete
|
||||
let warrantyCard = app.staticTexts[deleteTitle]
|
||||
XCTAssertTrue(warrantyCard.exists, "Warranty should exist")
|
||||
warrantyCard.tap()
|
||||
sleep(2)
|
||||
|
||||
let deleteButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Delete' OR label CONTAINS[c] 'trash'")).firstMatch
|
||||
if deleteButton.exists {
|
||||
deleteButton.tap()
|
||||
sleep(1)
|
||||
|
||||
// Confirm
|
||||
let confirmButton = app.alerts.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Delete'")).firstMatch
|
||||
if confirmButton.exists {
|
||||
confirmButton.tap()
|
||||
sleep(2)
|
||||
}
|
||||
|
||||
// Verify deleted
|
||||
sleep(2)
|
||||
let deletedCard = app.staticTexts[deleteTitle]
|
||||
XCTAssertFalse(deletedCard.exists, "Deleted warranty should not appear")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Edge Cases and Error Handling
|
||||
|
||||
func test19_CancelDocumentCreation() {
|
||||
navigateToDocumentsTab()
|
||||
switchToDocumentsTab()
|
||||
|
||||
XCTAssertTrue(openDocumentForm(), "Should open form")
|
||||
|
||||
// Fill some fields
|
||||
selectProperty() // REQUIRED - Select property first
|
||||
fillTextField(placeholder: "Title", text: "Cancelled Document")
|
||||
selectDocumentType(type: "Insurance")
|
||||
|
||||
// Cancel instead of save
|
||||
cancelForm()
|
||||
|
||||
// Should not appear in list
|
||||
sleep(2)
|
||||
let cancelledDoc = app.staticTexts["Cancelled Document"]
|
||||
XCTAssertFalse(cancelledDoc.exists, "Cancelled document should not be created")
|
||||
}
|
||||
|
||||
func test20_HandleEmptyDocumentsList() {
|
||||
navigateToDocumentsTab()
|
||||
switchToDocumentsTab()
|
||||
|
||||
// Apply very specific filter to get empty list
|
||||
searchFor(text: "NONEXISTENT_DOCUMENT_12345")
|
||||
|
||||
sleep(2)
|
||||
|
||||
// Should show empty state
|
||||
let emptyMessage = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'No documents' OR label CONTAINS[c] 'No results' OR label CONTAINS[c] 'empty'")).firstMatch
|
||||
|
||||
// Either empty state exists or no items are shown
|
||||
let hasNoItems = app.cells.count == 0
|
||||
XCTAssertTrue(emptyMessage.exists || hasNoItems, "Should handle empty documents list gracefully")
|
||||
|
||||
clearSearch()
|
||||
}
|
||||
|
||||
func test21_HandleEmptyWarrantiesList() {
|
||||
navigateToDocumentsTab()
|
||||
switchToWarrantiesTab()
|
||||
|
||||
// Search for non-existent warranty
|
||||
searchFor(text: "NONEXISTENT_WARRANTY_99999")
|
||||
|
||||
sleep(2)
|
||||
|
||||
let emptyMessage = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'No warranties' OR label CONTAINS[c] 'No results' OR label CONTAINS[c] 'empty'")).firstMatch
|
||||
let hasNoItems = app.cells.count == 0
|
||||
XCTAssertTrue(emptyMessage.exists || hasNoItems, "Should handle empty warranties list gracefully")
|
||||
|
||||
clearSearch()
|
||||
}
|
||||
|
||||
func test22_CreateDocumentWithLongTitle() {
|
||||
navigateToDocumentsTab()
|
||||
switchToDocumentsTab()
|
||||
|
||||
XCTAssertTrue(openDocumentForm(), "Should open form")
|
||||
|
||||
let longTitle = "This is a very long document title that exceeds normal length expectations to test how the UI handles lengthy text input " + UUID().uuidString
|
||||
createdDocumentTitles.append(longTitle)
|
||||
|
||||
selectProperty() // REQUIRED - Select property first
|
||||
fillTextField(placeholder: "Title", text: longTitle)
|
||||
selectDocumentType(type: "Insurance")
|
||||
|
||||
XCTAssertTrue(submitForm(), "Should handle long title")
|
||||
|
||||
sleep(2)
|
||||
// Just verify it was created (partial match)
|
||||
let partialTitle = String(longTitle.prefix(30))
|
||||
let documentExists = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] '\(partialTitle)'")).firstMatch.exists
|
||||
XCTAssertTrue(documentExists, "Document with long title should be created")
|
||||
}
|
||||
|
||||
func test23_CreateWarrantyWithSpecialCharacters() {
|
||||
navigateToDocumentsTab()
|
||||
switchToWarrantiesTab()
|
||||
|
||||
XCTAssertTrue(openDocumentForm(), "Should open form")
|
||||
|
||||
let specialTitle = "Warranty w/ Special #Chars: @ & $ % \(UUID().uuidString.prefix(8))"
|
||||
createdDocumentTitles.append(specialTitle)
|
||||
|
||||
selectProperty() // REQUIRED - Select property first
|
||||
fillTextField(placeholder: "Title", text: specialTitle)
|
||||
selectCategory(category: "Other")
|
||||
fillTextField(placeholder: "Item Name", text: "Test @#$ Item") // REQUIRED
|
||||
fillTextField(placeholder: "Provider", text: "Special & Co.") // REQUIRED
|
||||
|
||||
XCTAssertTrue(submitForm(), "Should handle special characters")
|
||||
|
||||
sleep(2)
|
||||
let partialTitle = String(specialTitle.prefix(20))
|
||||
let warrantyExists = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(partialTitle)'")).firstMatch.exists
|
||||
XCTAssertTrue(warrantyExists, "Warranty with special characters should be created")
|
||||
}
|
||||
|
||||
func test24_RapidTabSwitching() {
|
||||
navigateToDocumentsTab()
|
||||
|
||||
// Rapidly switch between tabs
|
||||
for _ in 0..<5 {
|
||||
switchToWarrantiesTab()
|
||||
usleep(500000) // 0.5 seconds
|
||||
switchToDocumentsTab()
|
||||
usleep(500000) // 0.5 seconds
|
||||
}
|
||||
|
||||
// Should remain stable
|
||||
let warrantiesTab = app.buttons["Warranties"]
|
||||
let documentsTab = app.buttons["Documents"]
|
||||
XCTAssertTrue(warrantiesTab.exists && documentsTab.exists, "Should handle rapid tab switching without crashing")
|
||||
}
|
||||
|
||||
func test25_MultipleFiltersCombined() {
|
||||
navigateToDocumentsTab()
|
||||
switchToWarrantiesTab()
|
||||
|
||||
// Apply multiple filters
|
||||
toggleActiveFilter() // Turn off active filter
|
||||
sleep(1)
|
||||
applyFilter(filterName: "Appliances")
|
||||
sleep(1)
|
||||
searchFor(text: "Test")
|
||||
|
||||
sleep(2)
|
||||
|
||||
// Should apply all filters without crashing
|
||||
let searchField = app.searchFields.firstMatch
|
||||
XCTAssertTrue(searchField.exists, "Should handle multiple filters simultaneously")
|
||||
|
||||
// Clean up
|
||||
clearSearch()
|
||||
sleep(1)
|
||||
applyFilter(filterName: "All Categories")
|
||||
sleep(1)
|
||||
toggleActiveFilter() // Turn active filter back on
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - XCUIElement Extension for Clearing Text
|
||||
|
||||
extension XCUIElement {
|
||||
func clearText() {
|
||||
guard let stringValue = self.value as? String else {
|
||||
return
|
||||
}
|
||||
|
||||
self.tap()
|
||||
|
||||
let deleteString = String(repeating: XCUIKeyboardKey.delete.rawValue, count: stringValue.count)
|
||||
self.typeText(deleteString)
|
||||
}
|
||||
}
|
||||
525
iosApp/CaseraUITests/Suite9_IntegrationE2ETests.swift
Normal file
525
iosApp/CaseraUITests/Suite9_IntegrationE2ETests.swift
Normal file
@@ -0,0 +1,525 @@
|
||||
import XCTest
|
||||
|
||||
/// Comprehensive End-to-End Integration Tests
|
||||
/// Mirrors the backend integration tests in myCribAPI-go/internal/integration/integration_test.go
|
||||
///
|
||||
/// This test suite covers:
|
||||
/// 1. Full authentication flow (register, login, logout)
|
||||
/// 2. Residence CRUD operations
|
||||
/// 3. Task lifecycle (create, update, mark-in-progress, complete, archive, cancel)
|
||||
/// 4. Residence sharing between users
|
||||
/// 5. Cross-user access control
|
||||
///
|
||||
/// IMPORTANT: These tests create real data and require network connectivity.
|
||||
/// Run with a test server or dev environment (not production).
|
||||
final class Suite9_IntegrationE2ETests: BaseUITestCase {
|
||||
override var includeResetStateLaunchArgument: Bool { false }
|
||||
|
||||
|
||||
// Test user credentials - unique per test run
|
||||
private let timestamp = Int(Date().timeIntervalSince1970)
|
||||
|
||||
private var userAUsername: String { "e2e_usera_\(timestamp)" }
|
||||
private var userAEmail: String { "e2e_usera_\(timestamp)@test.com" }
|
||||
private var userAPassword: String { "TestPass123!" }
|
||||
|
||||
private var userBUsername: String { "e2e_userb_\(timestamp)" }
|
||||
private var userBEmail: String { "e2e_userb_\(timestamp)@test.com" }
|
||||
private var userBPassword: String { "TestPass456!" }
|
||||
|
||||
/// Fixed verification code used by Go API when DEBUG=true
|
||||
private let verificationCode = "123456"
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
try super.tearDownWithError()
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
private func ensureLoggedOut() {
|
||||
UITestHelpers.ensureLoggedOut(app: app)
|
||||
}
|
||||
|
||||
private func login(username: String, password: String) {
|
||||
UITestHelpers.login(app: app, username: username, password: password)
|
||||
}
|
||||
|
||||
/// Navigate to a specific tab
|
||||
private func navigateToTab(_ tabName: String) {
|
||||
let tab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] '\(tabName)'")).firstMatch
|
||||
if tab.waitForExistence(timeout: 5) && !tab.isSelected {
|
||||
tab.tap()
|
||||
sleep(2)
|
||||
}
|
||||
}
|
||||
|
||||
/// Dismiss keyboard by tapping outside (doesn't submit forms)
|
||||
private func dismissKeyboard() {
|
||||
let coordinate = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.1))
|
||||
coordinate.tap()
|
||||
Thread.sleep(forTimeInterval: 0.5)
|
||||
}
|
||||
|
||||
/// Dismiss strong password suggestion if shown
|
||||
private func dismissStrongPasswordSuggestion() {
|
||||
let chooseOwnPassword = app.buttons["Choose My Own Password"]
|
||||
if chooseOwnPassword.waitForExistence(timeout: 1) {
|
||||
chooseOwnPassword.tap()
|
||||
return
|
||||
}
|
||||
let notNow = app.buttons["Not Now"]
|
||||
if notNow.exists && notNow.isHittable {
|
||||
notNow.tap()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Test 1: Complete Authentication Flow
|
||||
// Mirrors TestIntegration_AuthenticationFlow
|
||||
|
||||
func test01_authenticationFlow() {
|
||||
// Phase 1: Start on login screen
|
||||
let welcomeText = app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
|
||||
if !welcomeText.waitForExistence(timeout: 5) {
|
||||
ensureLoggedOut()
|
||||
}
|
||||
XCTAssertTrue(welcomeText.waitForExistence(timeout: 10), "Should start on login screen")
|
||||
|
||||
// Phase 2: Navigate to registration
|
||||
let signUpButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Sign Up'")).firstMatch
|
||||
XCTAssertTrue(signUpButton.waitForExistence(timeout: 5), "Sign Up button should exist")
|
||||
signUpButton.tap()
|
||||
sleep(2)
|
||||
|
||||
// Phase 3: Fill registration form using proper accessibility identifiers
|
||||
let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField]
|
||||
XCTAssertTrue(usernameField.waitForExistence(timeout: 5), "Username field should exist")
|
||||
usernameField.tap()
|
||||
usernameField.typeText(userAUsername)
|
||||
|
||||
let emailField = app.textFields[AccessibilityIdentifiers.Authentication.registerEmailField]
|
||||
XCTAssertTrue(emailField.waitForExistence(timeout: 3), "Email field should exist")
|
||||
emailField.tap()
|
||||
emailField.typeText(userAEmail)
|
||||
|
||||
// Password field - check both SecureField and TextField
|
||||
var passwordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerPasswordField]
|
||||
if !passwordField.exists {
|
||||
passwordField = app.textFields[AccessibilityIdentifiers.Authentication.registerPasswordField]
|
||||
}
|
||||
XCTAssertTrue(passwordField.waitForExistence(timeout: 3), "Password field should exist")
|
||||
passwordField.tap()
|
||||
dismissStrongPasswordSuggestion()
|
||||
passwordField.typeText(userAPassword)
|
||||
|
||||
// Confirm password field
|
||||
var confirmPasswordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerConfirmPasswordField]
|
||||
if !confirmPasswordField.exists {
|
||||
confirmPasswordField = app.textFields[AccessibilityIdentifiers.Authentication.registerConfirmPasswordField]
|
||||
}
|
||||
XCTAssertTrue(confirmPasswordField.waitForExistence(timeout: 3), "Confirm password field should exist")
|
||||
confirmPasswordField.tap()
|
||||
dismissStrongPasswordSuggestion()
|
||||
confirmPasswordField.typeText(userAPassword)
|
||||
|
||||
dismissKeyboard()
|
||||
sleep(1)
|
||||
|
||||
// Phase 4: Submit registration
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
|
||||
let registerButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton]
|
||||
XCTAssertTrue(registerButton.waitForExistence(timeout: 5), "Register button should exist")
|
||||
registerButton.tap()
|
||||
sleep(3)
|
||||
|
||||
// Phase 5: Handle email verification
|
||||
let verifyEmailTitle = app.staticTexts["Verify Your Email"]
|
||||
XCTAssertTrue(verifyEmailTitle.waitForExistence(timeout: 10), "Verification screen must appear after registration")
|
||||
|
||||
sleep(3)
|
||||
|
||||
// Enter verification code - auto-submits when 6 digits entered
|
||||
let codeField = app.textFields[AccessibilityIdentifiers.Authentication.verificationCodeField]
|
||||
XCTAssertTrue(codeField.waitForExistence(timeout: 5), "Verification code field must exist")
|
||||
codeField.tap()
|
||||
codeField.typeText(verificationCode)
|
||||
sleep(5)
|
||||
|
||||
// Phase 6: Verify logged in
|
||||
let tabBar = app.tabBars.firstMatch
|
||||
XCTAssertTrue(tabBar.waitForExistence(timeout: 15), "Should be logged in after registration")
|
||||
|
||||
// Phase 7: Logout
|
||||
UITestHelpers.logout(app: app)
|
||||
|
||||
// Phase 8: Login with created credentials
|
||||
XCTAssertTrue(welcomeText.waitForExistence(timeout: 10), "Should be on login screen after logout")
|
||||
login(username: userAUsername, password: userAPassword)
|
||||
|
||||
// Phase 9: Verify logged in
|
||||
XCTAssertTrue(tabBar.waitForExistence(timeout: 10), "Should be logged in after login")
|
||||
|
||||
// Phase 10: Final logout
|
||||
UITestHelpers.logout(app: app)
|
||||
XCTAssertTrue(welcomeText.waitForExistence(timeout: 10), "Should be logged out")
|
||||
}
|
||||
|
||||
// MARK: - Test 2: Residence CRUD Flow
|
||||
// Mirrors TestIntegration_ResidenceFlow
|
||||
|
||||
func test02_residenceCRUDFlow() {
|
||||
// Ensure logged in as test user
|
||||
UITestHelpers.ensureLoggedIn(app: app, username: userAUsername, password: userAPassword)
|
||||
navigateToTab("Residences")
|
||||
sleep(2)
|
||||
|
||||
let residenceName = "E2E Test Home \(timestamp)"
|
||||
|
||||
// Phase 1: Create residence
|
||||
let addButton = app.buttons[AccessibilityIdentifiers.Residence.addButton]
|
||||
XCTAssertTrue(addButton.waitForExistence(timeout: 5), "Add residence button should exist")
|
||||
addButton.tap()
|
||||
sleep(2)
|
||||
|
||||
// Fill form - just tap and type, don't dismiss keyboard between fields
|
||||
let nameField = app.textFields[AccessibilityIdentifiers.Residence.nameField]
|
||||
XCTAssertTrue(nameField.waitForExistence(timeout: 5), "Name field should exist")
|
||||
nameField.tap()
|
||||
sleep(1)
|
||||
nameField.typeText(residenceName)
|
||||
|
||||
// Use return key to move to next field or dismiss, then scroll
|
||||
app.keyboards.buttons["return"].tap()
|
||||
sleep(1)
|
||||
|
||||
// Scroll to show more fields
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
|
||||
// Fill street field
|
||||
let streetField = app.textFields[AccessibilityIdentifiers.Residence.streetAddressField]
|
||||
if streetField.waitForExistence(timeout: 3) && streetField.isHittable {
|
||||
streetField.tap()
|
||||
sleep(1)
|
||||
streetField.typeText("123 E2E Test St")
|
||||
app.keyboards.buttons["return"].tap()
|
||||
sleep(1)
|
||||
}
|
||||
|
||||
// Fill city field
|
||||
let cityField = app.textFields[AccessibilityIdentifiers.Residence.cityField]
|
||||
if cityField.waitForExistence(timeout: 3) && cityField.isHittable {
|
||||
cityField.tap()
|
||||
sleep(1)
|
||||
cityField.typeText("Austin")
|
||||
app.keyboards.buttons["return"].tap()
|
||||
sleep(1)
|
||||
}
|
||||
|
||||
// Fill state field
|
||||
let stateField = app.textFields[AccessibilityIdentifiers.Residence.stateProvinceField]
|
||||
if stateField.waitForExistence(timeout: 3) && stateField.isHittable {
|
||||
stateField.tap()
|
||||
sleep(1)
|
||||
stateField.typeText("TX")
|
||||
app.keyboards.buttons["return"].tap()
|
||||
sleep(1)
|
||||
}
|
||||
|
||||
// Fill postal code field
|
||||
let postalField = app.textFields[AccessibilityIdentifiers.Residence.postalCodeField]
|
||||
if postalField.waitForExistence(timeout: 3) && postalField.isHittable {
|
||||
postalField.tap()
|
||||
sleep(1)
|
||||
postalField.typeText("78701")
|
||||
}
|
||||
|
||||
// Dismiss keyboard and scroll to save button
|
||||
dismissKeyboard()
|
||||
sleep(1)
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
|
||||
// Save the residence
|
||||
let saveButton = app.buttons[AccessibilityIdentifiers.Residence.saveButton]
|
||||
if saveButton.waitForExistence(timeout: 5) && saveButton.isHittable {
|
||||
saveButton.tap()
|
||||
} else {
|
||||
// Try finding by label as fallback
|
||||
let saveByLabel = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch
|
||||
XCTAssertTrue(saveByLabel.waitForExistence(timeout: 5), "Save button should exist")
|
||||
saveByLabel.tap()
|
||||
}
|
||||
sleep(3)
|
||||
|
||||
// Phase 2: Verify residence was created
|
||||
navigateToTab("Residences")
|
||||
sleep(2)
|
||||
let residenceCard = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(residenceName)'")).firstMatch
|
||||
XCTAssertTrue(residenceCard.waitForExistence(timeout: 10), "Residence '\(residenceName)' should appear in list")
|
||||
}
|
||||
|
||||
// MARK: - Test 3: Task Lifecycle Flow
|
||||
// Mirrors TestIntegration_TaskFlow
|
||||
|
||||
func test03_taskLifecycleFlow() {
|
||||
// Ensure logged in
|
||||
UITestHelpers.ensureLoggedIn(app: app, username: userAUsername, password: userAPassword)
|
||||
|
||||
// Ensure residence exists first - create one if empty
|
||||
navigateToTab("Residences")
|
||||
sleep(2)
|
||||
|
||||
let residenceCards = app.cells
|
||||
if residenceCards.count == 0 {
|
||||
// No residences, create one first
|
||||
createMinimalResidence(name: "Task Test Home \(timestamp)")
|
||||
sleep(2)
|
||||
}
|
||||
|
||||
// Navigate to Tasks
|
||||
navigateToTab("Tasks")
|
||||
sleep(3)
|
||||
|
||||
let taskTitle = "E2E Task Lifecycle \(timestamp)"
|
||||
|
||||
// Phase 1: Create task - use firstMatch to avoid multiple element issue
|
||||
let addButton = app.buttons[AccessibilityIdentifiers.Task.addButton].firstMatch
|
||||
guard addButton.waitForExistence(timeout: 5) else {
|
||||
XCTFail("Add task button should exist")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if button is enabled
|
||||
guard addButton.isEnabled else {
|
||||
XCTFail("Add task button should be enabled (requires at least one residence)")
|
||||
return
|
||||
}
|
||||
|
||||
addButton.tap()
|
||||
sleep(2)
|
||||
|
||||
// Fill task form
|
||||
let titleField = app.textFields[AccessibilityIdentifiers.Task.titleField].firstMatch
|
||||
XCTAssertTrue(titleField.waitForExistence(timeout: 5), "Task title field should exist")
|
||||
titleField.tap()
|
||||
sleep(1)
|
||||
titleField.typeText(taskTitle)
|
||||
|
||||
dismissKeyboard()
|
||||
sleep(1)
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
|
||||
// Save the task
|
||||
let saveTaskButton = app.buttons[AccessibilityIdentifiers.Task.saveButton].firstMatch
|
||||
if saveTaskButton.waitForExistence(timeout: 5) && saveTaskButton.isHittable {
|
||||
saveTaskButton.tap()
|
||||
} else {
|
||||
let saveByLabel = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save' OR label CONTAINS[c] 'Add Task' OR label CONTAINS[c] 'Create'")).firstMatch
|
||||
XCTAssertTrue(saveByLabel.exists, "Save/Create button should exist")
|
||||
saveByLabel.tap()
|
||||
}
|
||||
sleep(3)
|
||||
|
||||
// Phase 2: Verify task was created
|
||||
navigateToTab("Tasks")
|
||||
sleep(2)
|
||||
let taskCard = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(taskTitle)'")).firstMatch
|
||||
XCTAssertTrue(taskCard.waitForExistence(timeout: 10), "Task '\(taskTitle)' should appear in task list")
|
||||
}
|
||||
|
||||
// MARK: - Test 4: Kanban Column Distribution
|
||||
// Mirrors TestIntegration_TasksByResidenceKanban
|
||||
|
||||
func test04_kanbanColumnDistribution() {
|
||||
UITestHelpers.ensureLoggedIn(app: app, username: userAUsername, password: userAPassword)
|
||||
navigateToTab("Tasks")
|
||||
sleep(3)
|
||||
|
||||
// Verify tasks screen is showing
|
||||
let tasksTitle = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch
|
||||
let kanbanExists = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Overdue' OR label CONTAINS[c] 'Upcoming' OR label CONTAINS[c] 'In Progress'")).firstMatch.exists
|
||||
|
||||
XCTAssertTrue(kanbanExists || tasksTitle.exists, "Tasks screen should be visible")
|
||||
}
|
||||
|
||||
// MARK: - Test 5: Cross-User Access Control
|
||||
// Mirrors TestIntegration_CrossUserAccessDenied
|
||||
|
||||
func test05_crossUserAccessControl() {
|
||||
UITestHelpers.ensureLoggedIn(app: app, username: userAUsername, password: userAPassword)
|
||||
|
||||
// Verify user can access their residences tab
|
||||
navigateToTab("Residences")
|
||||
sleep(2)
|
||||
|
||||
let residencesVisible = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch.isSelected
|
||||
XCTAssertTrue(residencesVisible, "User should be able to access Residences tab")
|
||||
|
||||
// Verify user can access their tasks tab
|
||||
navigateToTab("Tasks")
|
||||
sleep(2)
|
||||
|
||||
let tasksAccessible = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch.isSelected
|
||||
XCTAssertTrue(tasksAccessible, "User should be able to access Tasks tab")
|
||||
}
|
||||
|
||||
// MARK: - Test 6: Lookup Data Endpoints
|
||||
// Mirrors TestIntegration_LookupEndpoints
|
||||
|
||||
func test06_lookupDataAvailable() {
|
||||
UITestHelpers.ensureLoggedIn(app: app, username: userAUsername, password: userAPassword)
|
||||
|
||||
// Navigate to add residence to check residence types are loaded
|
||||
navigateToTab("Residences")
|
||||
sleep(2)
|
||||
|
||||
let addButton = app.buttons[AccessibilityIdentifiers.Residence.addButton]
|
||||
if addButton.waitForExistence(timeout: 5) {
|
||||
addButton.tap()
|
||||
sleep(2)
|
||||
|
||||
// Check property type picker exists (indicates lookups loaded)
|
||||
let propertyTypePicker = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Property Type' OR label CONTAINS[c] 'Type'")).firstMatch
|
||||
let pickerExists = propertyTypePicker.exists
|
||||
|
||||
// Cancel form
|
||||
let cancelButton = app.buttons[AccessibilityIdentifiers.Residence.formCancelButton]
|
||||
if cancelButton.exists {
|
||||
cancelButton.tap()
|
||||
} else {
|
||||
let cancelByLabel = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Cancel'")).firstMatch
|
||||
if cancelByLabel.exists {
|
||||
cancelByLabel.tap()
|
||||
}
|
||||
}
|
||||
|
||||
XCTAssertTrue(pickerExists, "Property type picker should exist (lookups loaded)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Test 7: Residence Sharing Flow
|
||||
// Mirrors TestIntegration_ResidenceSharingFlow
|
||||
|
||||
func test07_residenceSharingUIElements() {
|
||||
UITestHelpers.ensureLoggedIn(app: app, username: userAUsername, password: userAPassword)
|
||||
navigateToTab("Residences")
|
||||
sleep(2)
|
||||
|
||||
// Find any residence to check sharing UI
|
||||
let residenceCard = app.cells.firstMatch
|
||||
if residenceCard.waitForExistence(timeout: 5) {
|
||||
residenceCard.tap()
|
||||
sleep(2)
|
||||
|
||||
// Look for share button in residence details
|
||||
let shareButton = app.buttons[AccessibilityIdentifiers.Residence.shareButton]
|
||||
let manageUsersButton = app.buttons[AccessibilityIdentifiers.Residence.manageUsersButton]
|
||||
|
||||
// Note: Share functionality may not be visible depending on user permissions
|
||||
// This test just verifies we can navigate to residence details
|
||||
|
||||
// Navigate back
|
||||
let backButton = app.navigationBars.buttons.element(boundBy: 0)
|
||||
if backButton.exists && backButton.isHittable {
|
||||
backButton.tap()
|
||||
sleep(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper: Create Minimal Residence
|
||||
|
||||
private func createMinimalResidence(name: String) {
|
||||
let addButton = app.buttons[AccessibilityIdentifiers.Residence.addButton]
|
||||
guard addButton.waitForExistence(timeout: 5) else { return }
|
||||
|
||||
addButton.tap()
|
||||
sleep(2)
|
||||
|
||||
// Fill name field
|
||||
let nameField = app.textFields[AccessibilityIdentifiers.Residence.nameField]
|
||||
if nameField.waitForExistence(timeout: 5) {
|
||||
nameField.tap()
|
||||
sleep(1)
|
||||
nameField.typeText(name)
|
||||
app.keyboards.buttons["return"].tap()
|
||||
sleep(1)
|
||||
}
|
||||
|
||||
// Scroll to show address fields
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
|
||||
// Fill street field
|
||||
let streetField = app.textFields[AccessibilityIdentifiers.Residence.streetAddressField]
|
||||
if streetField.waitForExistence(timeout: 3) && streetField.isHittable {
|
||||
streetField.tap()
|
||||
sleep(1)
|
||||
streetField.typeText("123 Test St")
|
||||
app.keyboards.buttons["return"].tap()
|
||||
sleep(1)
|
||||
}
|
||||
|
||||
// Fill city field
|
||||
let cityField = app.textFields[AccessibilityIdentifiers.Residence.cityField]
|
||||
if cityField.waitForExistence(timeout: 3) && cityField.isHittable {
|
||||
cityField.tap()
|
||||
sleep(1)
|
||||
cityField.typeText("Austin")
|
||||
app.keyboards.buttons["return"].tap()
|
||||
sleep(1)
|
||||
}
|
||||
|
||||
// Fill state field
|
||||
let stateField = app.textFields[AccessibilityIdentifiers.Residence.stateProvinceField]
|
||||
if stateField.waitForExistence(timeout: 3) && stateField.isHittable {
|
||||
stateField.tap()
|
||||
sleep(1)
|
||||
stateField.typeText("TX")
|
||||
app.keyboards.buttons["return"].tap()
|
||||
sleep(1)
|
||||
}
|
||||
|
||||
// Fill postal code field
|
||||
let postalField = app.textFields[AccessibilityIdentifiers.Residence.postalCodeField]
|
||||
if postalField.waitForExistence(timeout: 3) && postalField.isHittable {
|
||||
postalField.tap()
|
||||
sleep(1)
|
||||
postalField.typeText("78701")
|
||||
}
|
||||
|
||||
dismissKeyboard()
|
||||
sleep(1)
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
|
||||
// Save
|
||||
let saveButton = app.buttons[AccessibilityIdentifiers.Residence.saveButton]
|
||||
if saveButton.waitForExistence(timeout: 5) && saveButton.isHittable {
|
||||
saveButton.tap()
|
||||
} else {
|
||||
let saveByLabel = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch
|
||||
if saveByLabel.exists {
|
||||
saveByLabel.tap()
|
||||
}
|
||||
}
|
||||
sleep(3)
|
||||
}
|
||||
|
||||
// MARK: - Helper: Find Add Task Button
|
||||
|
||||
private func findAddTaskButton() -> XCUIElement {
|
||||
let addButton = app.buttons[AccessibilityIdentifiers.Task.addButton]
|
||||
if addButton.exists {
|
||||
return addButton
|
||||
}
|
||||
return app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Add Task' OR label CONTAINS[c] 'New Task'")).firstMatch
|
||||
}
|
||||
}
|
||||
33
iosApp/CaseraUITests/Tests/AccessibilityTests.swift
Normal file
33
iosApp/CaseraUITests/Tests/AccessibilityTests.swift
Normal file
@@ -0,0 +1,33 @@
|
||||
import XCTest
|
||||
|
||||
final class AccessibilityTests: BaseUITestCase {
|
||||
func testA001_OnboardingPrimaryControlsAreReachable() {
|
||||
let welcome = OnboardingWelcomeScreen(app: app)
|
||||
welcome.waitForLoad()
|
||||
|
||||
app.buttons[UITestID.Onboarding.startFreshButton].waitUntilHittable(timeout: defaultTimeout)
|
||||
app.buttons[UITestID.Onboarding.joinExistingButton].waitUntilHittable(timeout: defaultTimeout)
|
||||
app.buttons[UITestID.Onboarding.loginButton].waitUntilHittable(timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func testA002_LoginControlsRemainOperable() {
|
||||
let login = TestFlows.navigateToLoginFromOnboarding(app: app)
|
||||
|
||||
app.textFields[UITestID.Auth.usernameField].waitUntilHittable(timeout: defaultTimeout)
|
||||
app.secureTextFields[UITestID.Auth.passwordField].waitUntilHittable(timeout: defaultTimeout)
|
||||
app.buttons[UITestID.Auth.loginButton].waitUntilHittable(timeout: defaultTimeout)
|
||||
|
||||
login.tapPasswordVisibilityToggle()
|
||||
login.assertPasswordFieldVisible()
|
||||
}
|
||||
|
||||
func testA003_CoreControlsExposeIdentifiers() {
|
||||
let login = TestFlows.navigateToLoginFromOnboarding(app: app)
|
||||
_ = login
|
||||
|
||||
XCTAssertTrue(app.textFields[UITestID.Auth.usernameField].exists)
|
||||
XCTAssertTrue(app.secureTextFields[UITestID.Auth.passwordField].exists || app.textFields[UITestID.Auth.passwordField].exists)
|
||||
XCTAssertTrue(app.buttons[UITestID.Auth.signUpButton].exists)
|
||||
XCTAssertTrue(app.buttons[UITestID.Auth.forgotPasswordButton].exists)
|
||||
}
|
||||
}
|
||||
19
iosApp/CaseraUITests/Tests/AppLaunchTests.swift
Normal file
19
iosApp/CaseraUITests/Tests/AppLaunchTests.swift
Normal file
@@ -0,0 +1,19 @@
|
||||
import XCTest
|
||||
|
||||
final class AppLaunchTests: BaseUITestCase {
|
||||
func testF001_ColdLaunchShowsOnboardingWelcome() {
|
||||
RootScreen(app: app).waitForReady(timeout: defaultTimeout)
|
||||
|
||||
let welcome = OnboardingWelcomeScreen(app: app)
|
||||
welcome.waitForLoad(timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func testF002_ColdLaunchShowsPrimaryOnboardingActions() {
|
||||
let welcome = OnboardingWelcomeScreen(app: app)
|
||||
welcome.waitForLoad()
|
||||
|
||||
XCTAssertTrue(app.buttons[UITestID.Onboarding.startFreshButton].exists)
|
||||
XCTAssertTrue(app.buttons[UITestID.Onboarding.joinExistingButton].exists)
|
||||
XCTAssertTrue(app.buttons[UITestID.Onboarding.loginButton].exists)
|
||||
}
|
||||
}
|
||||
31
iosApp/CaseraUITests/Tests/AuthenticationTests.swift
Normal file
31
iosApp/CaseraUITests/Tests/AuthenticationTests.swift
Normal file
@@ -0,0 +1,31 @@
|
||||
import XCTest
|
||||
|
||||
final class AuthenticationTests: BaseUITestCase {
|
||||
func testF201_OnboardingLoginEntryShowsLoginScreen() {
|
||||
let login = TestFlows.navigateToLoginFromOnboarding(app: app)
|
||||
login.waitForLoad(timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func testF202_LoginScreenCanTogglePasswordVisibility() {
|
||||
let login = TestFlows.navigateToLoginFromOnboarding(app: app)
|
||||
login.enterUsername("u")
|
||||
login.enterPassword("p")
|
||||
login.tapPasswordVisibilityToggle()
|
||||
login.assertPasswordFieldVisible()
|
||||
}
|
||||
|
||||
func testF203_RegisterSheetCanOpenAndDismiss() {
|
||||
let register = TestFlows.openRegisterFromLogin(app: app)
|
||||
register.tapCancel()
|
||||
|
||||
let login = LoginScreen(app: app)
|
||||
login.waitForLoad(timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func testF204_RegisterFormAcceptsInput() {
|
||||
let register = TestFlows.openRegisterFromLogin(app: app)
|
||||
register.waitForLoad(timeout: defaultTimeout)
|
||||
|
||||
XCTAssertTrue(app.buttons[UITestID.Auth.registerButton].exists)
|
||||
}
|
||||
}
|
||||
33
iosApp/CaseraUITests/Tests/OnboardingTests.swift
Normal file
33
iosApp/CaseraUITests/Tests/OnboardingTests.swift
Normal file
@@ -0,0 +1,33 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import XCTest
|
||||
|
||||
/// Rebuild plan for legacy: Suite0_OnboardingTests.test_onboarding
|
||||
/// Split into smaller tests to isolate focus/input/navigation failures.
|
||||
final class Suite0_OnboardingRebuildTests: BaseUITestCase {
|
||||
func testR001_onboardingWelcomeLoadsAndCanNavigateToLoginEntry() {
|
||||
let welcome = OnboardingWelcomeScreen(app: app)
|
||||
welcome.waitForLoad(timeout: defaultTimeout)
|
||||
welcome.tapAlreadyHaveAccount()
|
||||
|
||||
let login = LoginScreen(app: app)
|
||||
login.waitForLoad(timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func testR002_startFreshFlowReachesCreateAccount() {
|
||||
let createAccount = TestFlows.navigateStartFreshToCreateAccount(app: app, residenceName: "Rebuild Home")
|
||||
createAccount.waitForLoad(timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func testR003_createAccountExpandedFormFieldsAreInteractable() throws {
|
||||
throw XCTSkip("Skeleton: implement deterministic focus assertions for username/email/password fields")
|
||||
}
|
||||
|
||||
func testR004_emailFieldCanFocusAndAcceptTyping() throws {
|
||||
throw XCTSkip("Skeleton: implement replacement for legacy email focus failure")
|
||||
}
|
||||
|
||||
func testR005_createAccountContinueOnlyAfterValidInputs() throws {
|
||||
throw XCTSkip("Skeleton: validate disabled/enabled state transition for Create Account")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import XCTest
|
||||
|
||||
/// Rebuild plan for legacy failures in Suite1_RegistrationTests:
|
||||
/// - test07, test09, test10, test11, test12
|
||||
/// Coverage is split into smaller tests for easier isolation.
|
||||
final class Suite1_RegistrationRebuildTests: BaseUITestCase {
|
||||
override var includeResetStateLaunchArgument: Bool { false }
|
||||
|
||||
func testR101_registerFormCanOpenFromLogin() {
|
||||
UITestHelpers.ensureOnLoginScreen(app: app)
|
||||
let register = TestFlows.openRegisterFromLogin(app: app)
|
||||
register.waitForLoad(timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func testR102_registerFormAcceptsValidInput() {
|
||||
UITestHelpers.ensureOnLoginScreen(app: app)
|
||||
let register = TestFlows.openRegisterFromLogin(app: app)
|
||||
XCTAssertTrue(app.textFields[UITestID.Auth.registerUsernameField].exists)
|
||||
XCTAssertTrue(app.textFields[UITestID.Auth.registerEmailField].exists)
|
||||
XCTAssertTrue(app.secureTextFields[UITestID.Auth.registerPasswordField].exists)
|
||||
XCTAssertTrue(app.secureTextFields[UITestID.Auth.registerConfirmPasswordField].exists)
|
||||
XCTAssertTrue(app.buttons[UITestID.Auth.registerButton].exists)
|
||||
}
|
||||
|
||||
func testR103_successfulRegistrationTransitionsToVerificationGate() throws {
|
||||
throw XCTSkip("Skeleton: submit valid registration and assert verification gate")
|
||||
}
|
||||
|
||||
func testR104_verificationGateBlocksMainAppBeforeCodeEntry() throws {
|
||||
throw XCTSkip("Skeleton: assert no tab bar access while unverified")
|
||||
}
|
||||
|
||||
func testR105_validVerificationCodeTransitionsToMainApp() throws {
|
||||
throw XCTSkip("Skeleton: use deterministic verification code fixture and assert main app root")
|
||||
}
|
||||
|
||||
func testR106_mainAppSessionAfterVerificationCanReachProfile() throws {
|
||||
throw XCTSkip("Skeleton: assert verified user can navigate tab bar and profile")
|
||||
}
|
||||
|
||||
func testR107_invalidVerificationCodeShowsErrorAndStaysBlocked() throws {
|
||||
throw XCTSkip("Skeleton: replacement for legacy test09")
|
||||
}
|
||||
|
||||
func testR108_incompleteVerificationCodeDoesNotCompleteVerification() throws {
|
||||
throw XCTSkip("Skeleton: replacement for legacy test10")
|
||||
}
|
||||
|
||||
func testR109_verifyButtonDisabledForIncompleteCode() throws {
|
||||
throw XCTSkip("Skeleton: optional split from legacy test10 button state assertion")
|
||||
}
|
||||
|
||||
func testR110_relaunchUnverifiedUserNeverLandsInMainApp() throws {
|
||||
throw XCTSkip("Skeleton: replacement for legacy test11")
|
||||
}
|
||||
|
||||
func testR111_relaunchUnverifiedUserResumesVerificationOrLoginGate() throws {
|
||||
throw XCTSkip("Skeleton: acceptable states after relaunch")
|
||||
}
|
||||
|
||||
func testR112_logoutFromVerificationReturnsToLogin() throws {
|
||||
throw XCTSkip("Skeleton: replacement for legacy test12")
|
||||
}
|
||||
|
||||
func testR113_verificationElementsDisappearAfterLogout() throws {
|
||||
throw XCTSkip("Skeleton: split assertion from legacy test12")
|
||||
}
|
||||
|
||||
func testR114_logoutFromVerifiedMainAppReturnsToLogin() throws {
|
||||
throw XCTSkip("Skeleton: split assertion from legacy test07 cleanup")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import XCTest
|
||||
|
||||
/// Rebuild plan for legacy Suite2 failures:
|
||||
/// - test02_loginWithValidCredentials
|
||||
/// - test06_logout
|
||||
final class Suite2_AuthenticationRebuildTests: BaseUITestCase {
|
||||
override var includeResetStateLaunchArgument: Bool { false }
|
||||
override var additionalLaunchArguments: [String] { ["--ui-test-mock-auth"] }
|
||||
private let validUser = RebuildTestUserFactory.seeded
|
||||
|
||||
private enum AuthLandingState {
|
||||
case main
|
||||
case verification
|
||||
}
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
UITestHelpers.ensureLoggedOut(app: app)
|
||||
}
|
||||
|
||||
private func loginFromLoginScreen(user: RebuildTestUser = RebuildTestUserFactory.seeded) {
|
||||
UITestHelpers.ensureOnLoginScreen(app: app)
|
||||
let login = LoginScreen(app: app)
|
||||
login.waitForLoad(timeout: defaultTimeout)
|
||||
login.enterUsername(user.username)
|
||||
login.enterPassword(user.password)
|
||||
|
||||
let loginButton = app.buttons[AccessibilityIdentifiers.Authentication.loginButton]
|
||||
loginButton.waitForExistenceOrFail(timeout: defaultTimeout)
|
||||
loginButton.forceTap()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func loginAndWaitForAuthenticatedLanding(user: RebuildTestUser = RebuildTestUserFactory.seeded) -> AuthLandingState {
|
||||
loginFromLoginScreen(user: user)
|
||||
|
||||
let mainRoot = app.otherElements[UITestID.Root.mainTabs]
|
||||
if mainRoot.waitForExistence(timeout: longTimeout) || app.tabBars.firstMatch.waitForExistence(timeout: 2) {
|
||||
return .main
|
||||
}
|
||||
|
||||
let verification = VerificationScreen(app: app)
|
||||
if verification.codeField.waitForExistence(timeout: defaultTimeout) || verification.verifyButton.waitForExistence(timeout: 2) {
|
||||
return .verification
|
||||
}
|
||||
|
||||
XCTFail("Expected authenticated landing on main tabs or verification screen")
|
||||
return .verification
|
||||
}
|
||||
|
||||
private func logoutFromVerificationIfNeeded() {
|
||||
let verification = VerificationScreen(app: app)
|
||||
verification.waitForLoad(timeout: defaultTimeout)
|
||||
verification.tapLogoutIfAvailable()
|
||||
|
||||
let toolbarLogout = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Logout' OR label CONTAINS[c] 'Log Out'")).firstMatch
|
||||
if toolbarLogout.waitForExistence(timeout: 3) {
|
||||
toolbarLogout.forceTap()
|
||||
}
|
||||
}
|
||||
|
||||
private func logoutFromMainApp() {
|
||||
UITestHelpers.logout(app: app)
|
||||
}
|
||||
|
||||
func testR201_loginScreenLoadsFromOnboardingEntry() {
|
||||
UITestHelpers.ensureOnLoginScreen(app: app)
|
||||
let login = LoginScreen(app: app)
|
||||
login.waitForLoad(timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func testR202_validCredentialsSubmitFromLogin() {
|
||||
UITestHelpers.ensureOnLoginScreen(app: app)
|
||||
let login = LoginScreen(app: app)
|
||||
login.waitForLoad(timeout: defaultTimeout)
|
||||
|
||||
login.enterUsername(validUser.username)
|
||||
login.enterPassword(validUser.password)
|
||||
|
||||
let loginButton = app.buttons[AccessibilityIdentifiers.Authentication.loginButton]
|
||||
XCTAssertTrue(loginButton.waitForExistence(timeout: defaultTimeout), "Login button must exist before submit")
|
||||
XCTAssertTrue(loginButton.isHittable, "Login button must be tappable")
|
||||
}
|
||||
|
||||
func testR203_validLoginTransitionsToMainAppRoot() {
|
||||
let landing = loginAndWaitForAuthenticatedLanding(user: validUser)
|
||||
switch landing {
|
||||
case .main:
|
||||
RebuildSessionAssertions.assertOnMainApp(app, timeout: longTimeout)
|
||||
case .verification:
|
||||
RebuildSessionAssertions.assertOnVerification(app, timeout: longTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
func testR204_mainAppHasExpectedPrimaryTabsAfterLogin() {
|
||||
let landing = loginAndWaitForAuthenticatedLanding(user: validUser)
|
||||
|
||||
switch landing {
|
||||
case .main:
|
||||
RebuildSessionAssertions.assertOnMainApp(app, timeout: longTimeout)
|
||||
|
||||
let tabBar = app.tabBars.firstMatch
|
||||
if tabBar.waitForExistence(timeout: 5) {
|
||||
let residences = tabBar.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
|
||||
let tasks = tabBar.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch
|
||||
let contractors = tabBar.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Contractors'")).firstMatch
|
||||
let docs = tabBar.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Doc'")).firstMatch
|
||||
XCTAssertTrue(residences.exists, "Residences tab should exist")
|
||||
XCTAssertTrue(tasks.exists, "Tasks tab should exist")
|
||||
XCTAssertTrue(contractors.exists, "Contractors tab should exist")
|
||||
XCTAssertTrue(docs.exists, "Documents tab should exist")
|
||||
} else {
|
||||
XCTAssertTrue(app.otherElements[UITestID.Root.mainTabs].exists, "Main tabs root should exist")
|
||||
}
|
||||
case .verification:
|
||||
let verify = VerificationScreen(app: app)
|
||||
verify.waitForLoad(timeout: defaultTimeout)
|
||||
XCTAssertTrue(verify.codeField.exists, "Verification code field should exist for unverified accounts")
|
||||
}
|
||||
}
|
||||
|
||||
func testR205_logoutFromMainAppReturnsToLoginRoot() {
|
||||
let landing = loginAndWaitForAuthenticatedLanding(user: validUser)
|
||||
|
||||
switch landing {
|
||||
case .main:
|
||||
logoutFromMainApp()
|
||||
case .verification:
|
||||
logoutFromVerificationIfNeeded()
|
||||
}
|
||||
RebuildSessionAssertions.assertOnLogin(app, timeout: longTimeout)
|
||||
}
|
||||
|
||||
func testR206_postLogoutMainAppIsNoLongerAccessible() {
|
||||
let landing = loginAndWaitForAuthenticatedLanding(user: validUser)
|
||||
|
||||
switch landing {
|
||||
case .main:
|
||||
logoutFromMainApp()
|
||||
case .verification:
|
||||
logoutFromVerificationIfNeeded()
|
||||
}
|
||||
RebuildSessionAssertions.assertOnLogin(app, timeout: longTimeout)
|
||||
|
||||
XCTAssertFalse(app.otherElements[UITestID.Root.mainTabs].exists, "Main app root should not be visible after logout")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import XCTest
|
||||
|
||||
/// Rebuild plan for legacy Suite3 failures (all blocked at residences tab precondition).
|
||||
/// Old tests covered:
|
||||
/// - test01_viewResidencesList
|
||||
/// - test02_navigateToAddResidence
|
||||
/// - test03_navigationBetweenTabs
|
||||
/// - test04_cancelResidenceCreation
|
||||
/// - test05_createResidenceWithMinimalData
|
||||
/// - test06_viewResidenceDetails
|
||||
final class Suite3_ResidenceRebuildTests: BaseUITestCase {
|
||||
override var includeResetStateLaunchArgument: Bool { false }
|
||||
override var additionalLaunchArguments: [String] { ["--ui-test-mock-auth"] }
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
UITestHelpers.ensureLoggedOut(app: app)
|
||||
}
|
||||
|
||||
private func loginAndOpenResidences() {
|
||||
UITestHelpers.ensureOnLoginScreen(app: app)
|
||||
let login = LoginScreen(app: app)
|
||||
login.waitForLoad(timeout: defaultTimeout)
|
||||
login.enterUsername("testuser")
|
||||
login.enterPassword("TestPass123!")
|
||||
app.buttons[AccessibilityIdentifiers.Authentication.loginButton].waitForExistenceOrFail(timeout: defaultTimeout).forceTap()
|
||||
|
||||
let main = MainTabScreen(app: app)
|
||||
main.waitForLoad(timeout: longTimeout)
|
||||
main.goToResidences()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func createResidence(name: String) -> String {
|
||||
loginAndOpenResidences()
|
||||
|
||||
let list = ResidenceListScreen(app: app)
|
||||
list.waitForLoad(timeout: defaultTimeout)
|
||||
list.openCreateResidence()
|
||||
|
||||
let form = ResidenceFormScreen(app: app)
|
||||
form.waitForLoad(timeout: defaultTimeout)
|
||||
form.enterName(name)
|
||||
|
||||
form.save()
|
||||
return name
|
||||
}
|
||||
|
||||
func testR301_authenticatedPreconditionCanReachMainApp() throws {
|
||||
loginAndOpenResidences()
|
||||
RebuildSessionAssertions.assertOnMainApp(app, timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func testR302_residencesTabIsPresentAndNavigable() throws {
|
||||
loginAndOpenResidences()
|
||||
let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
|
||||
XCTAssertTrue(residencesTab.exists, "Residences tab should exist")
|
||||
}
|
||||
|
||||
func testR303_residencesListLoadsAfterTabSelection() throws {
|
||||
loginAndOpenResidences()
|
||||
let list = ResidenceListScreen(app: app)
|
||||
list.waitForLoad(timeout: defaultTimeout)
|
||||
XCTAssertTrue(list.addButton.exists, "Add residence button should be visible")
|
||||
}
|
||||
|
||||
func testR304_openAddResidenceFormFromResidencesList() throws {
|
||||
loginAndOpenResidences()
|
||||
let list = ResidenceListScreen(app: app)
|
||||
list.waitForLoad(timeout: defaultTimeout)
|
||||
list.openCreateResidence()
|
||||
|
||||
let form = ResidenceFormScreen(app: app)
|
||||
form.waitForLoad(timeout: defaultTimeout)
|
||||
XCTAssertTrue(form.saveButton.exists, "Residence save button should exist")
|
||||
}
|
||||
|
||||
func testR305_cancelAddResidenceReturnsToResidenceList() throws {
|
||||
loginAndOpenResidences()
|
||||
let list = ResidenceListScreen(app: app)
|
||||
list.openCreateResidence()
|
||||
|
||||
let form = ResidenceFormScreen(app: app)
|
||||
form.waitForLoad(timeout: defaultTimeout)
|
||||
form.cancel()
|
||||
|
||||
list.waitForLoad(timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
func testR306_createResidenceMinimalDataSubmitsSuccessfully() throws {
|
||||
let name = "UITest Home \(Int(Date().timeIntervalSince1970))"
|
||||
_ = createResidence(name: name)
|
||||
let created = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] %@", name)).firstMatch
|
||||
XCTAssertTrue(created.waitForExistence(timeout: longTimeout), "Created residence should appear in list")
|
||||
}
|
||||
|
||||
func testR307_newResidenceAppearsInResidenceList() throws {
|
||||
let name = "UITest Verify \(Int(Date().timeIntervalSince1970))"
|
||||
_ = createResidence(name: name)
|
||||
let created = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] %@", name)).firstMatch
|
||||
XCTAssertTrue(created.waitForExistence(timeout: longTimeout), "New residence should be visible in residences list")
|
||||
}
|
||||
|
||||
func testR308_openResidenceDetailsFromResidenceList() throws {
|
||||
let name = "UITest Detail \(Int(Date().timeIntervalSince1970))"
|
||||
_ = createResidence(name: name)
|
||||
|
||||
let row = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] %@", name)).firstMatch
|
||||
row.waitForExistenceOrFail(timeout: longTimeout).forceTap()
|
||||
|
||||
let edit = app.buttons[AccessibilityIdentifiers.Residence.editButton]
|
||||
let delete = app.buttons[AccessibilityIdentifiers.Residence.deleteButton]
|
||||
let loaded = edit.waitForExistence(timeout: defaultTimeout) || delete.waitForExistence(timeout: defaultTimeout)
|
||||
XCTAssertTrue(loaded, "Residence details should expose edit or delete actions")
|
||||
}
|
||||
|
||||
func testR309_navigationAcrossPrimaryTabsAndBackToResidences() throws {
|
||||
loginAndOpenResidences()
|
||||
|
||||
let tabBar = app.tabBars.firstMatch
|
||||
tabBar.waitForExistenceOrFail(timeout: defaultTimeout)
|
||||
|
||||
let tasksTab = tabBar.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch
|
||||
XCTAssertTrue(tasksTab.exists, "Tasks tab should exist")
|
||||
tasksTab.forceTap()
|
||||
|
||||
let contractorsTab = tabBar.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Contractors'")).firstMatch
|
||||
XCTAssertTrue(contractorsTab.exists, "Contractors tab should exist")
|
||||
contractorsTab.forceTap()
|
||||
|
||||
let residencesTab = tabBar.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
|
||||
residencesTab.forceTap()
|
||||
|
||||
let list = ResidenceListScreen(app: app)
|
||||
list.waitForLoad(timeout: defaultTimeout)
|
||||
}
|
||||
}
|
||||
39
iosApp/CaseraUITests/Tests/StabilityTests.swift
Normal file
39
iosApp/CaseraUITests/Tests/StabilityTests.swift
Normal file
@@ -0,0 +1,39 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +1,58 @@
|
||||
import XCTest
|
||||
|
||||
/// Reusable helper functions for UI tests.
|
||||
/// All waits use explicit conditions — zero sleep() calls.
|
||||
/// Reusable helper functions for UI tests
|
||||
struct UITestHelpers {
|
||||
private static func loginUsernameField(app: XCUIApplication) -> XCUIElement {
|
||||
app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
|
||||
}
|
||||
|
||||
// MARK: - Authentication Helpers
|
||||
|
||||
/// Logs out the user if they are currently logged in.
|
||||
/// Logs out the user if they are currently logged in
|
||||
/// - Parameter app: The XCUIApplication instance
|
||||
static func logout(app: XCUIApplication) {
|
||||
// Check if already logged out (login screen visible)
|
||||
let welcomeText = app.staticTexts["Welcome Back"]
|
||||
if welcomeText.waitForExistence(timeout: 3) {
|
||||
sleep(1)
|
||||
|
||||
// Already on login screen.
|
||||
let usernameField = loginUsernameField(app: app)
|
||||
if usernameField.waitForExistence(timeout: 2) {
|
||||
return
|
||||
}
|
||||
|
||||
// In onboarding flow, navigate to login.
|
||||
let onboardingRoot = app.otherElements[UITestID.Root.onboarding]
|
||||
if onboardingRoot.waitForExistence(timeout: 2) {
|
||||
ensureOnLoginScreen(app: app)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we have a tab bar (logged in state)
|
||||
let tabBar = app.tabBars.firstMatch
|
||||
guard tabBar.waitForExistence(timeout: 3) else { return }
|
||||
guard tabBar.exists else { return }
|
||||
|
||||
// Navigate to Residences tab first
|
||||
let residencesTab = app.tabBars.buttons[AccessibilityIdentifiers.Navigation.residencesTab]
|
||||
let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
|
||||
if residencesTab.exists {
|
||||
residencesTab.tap()
|
||||
sleep(1)
|
||||
}
|
||||
|
||||
// Tap settings button
|
||||
let settingsButton = app.buttons[AccessibilityIdentifiers.Navigation.settingsButton]
|
||||
if settingsButton.waitForExistence(timeout: 5) {
|
||||
if settingsButton.waitForExistence(timeout: 3) && settingsButton.isHittable {
|
||||
settingsButton.tap()
|
||||
sleep(1)
|
||||
}
|
||||
|
||||
// Find and tap logout button
|
||||
let logoutButton = app.buttons[AccessibilityIdentifiers.Profile.logoutButton]
|
||||
if logoutButton.waitForExistence(timeout: 5) {
|
||||
if logoutButton.waitForExistence(timeout: 3) {
|
||||
logoutButton.tap()
|
||||
sleep(1)
|
||||
|
||||
// Confirm logout in alert if present
|
||||
// Confirm logout in alert if present - specifically target the alert's button
|
||||
let alert = app.alerts.firstMatch
|
||||
if alert.waitForExistence(timeout: 3) {
|
||||
if alert.waitForExistence(timeout: 2) {
|
||||
let confirmLogout = alert.buttons["Log Out"]
|
||||
if confirmLogout.exists {
|
||||
confirmLogout.tap()
|
||||
@@ -45,21 +60,27 @@ struct UITestHelpers {
|
||||
}
|
||||
}
|
||||
|
||||
// Verify we're back on login screen
|
||||
sleep(2)
|
||||
|
||||
XCTAssertTrue(
|
||||
welcomeText.waitForExistence(timeout: 10),
|
||||
"Failed to log out - Welcome Back screen should appear after logout"
|
||||
usernameField.waitForExistence(timeout: 8),
|
||||
"Failed to log out - login username field should appear"
|
||||
)
|
||||
}
|
||||
|
||||
/// Logs in a user with the provided credentials.
|
||||
/// Logs in a user with the provided credentials
|
||||
/// - Parameters:
|
||||
/// - app: The XCUIApplication instance
|
||||
/// - username: The username/email to use for login
|
||||
/// - password: The password to use for login
|
||||
static func login(app: XCUIApplication, username: String, password: String) {
|
||||
// Find username field by accessibility identifier
|
||||
let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
|
||||
XCTAssertTrue(usernameField.waitForExistence(timeout: 5), "Username field should exist")
|
||||
usernameField.tap()
|
||||
usernameField.typeText(username)
|
||||
|
||||
// Password field may be SecureTextField or regular TextField
|
||||
// Find password field - it could be TextField (if visible) or SecureField
|
||||
var passwordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.passwordField]
|
||||
if !passwordField.exists {
|
||||
passwordField = app.textFields[AccessibilityIdentifiers.Authentication.passwordField]
|
||||
@@ -68,37 +89,86 @@ struct UITestHelpers {
|
||||
passwordField.tap()
|
||||
passwordField.typeText(password)
|
||||
|
||||
// Find and tap login button
|
||||
let loginButton = app.buttons[AccessibilityIdentifiers.Authentication.loginButton]
|
||||
XCTAssertTrue(loginButton.waitForExistence(timeout: 3), "Login button should exist")
|
||||
loginButton.tap()
|
||||
|
||||
// Wait for login to complete by checking for tab bar appearance
|
||||
let tabBar = app.tabBars.firstMatch
|
||||
_ = tabBar.waitForExistence(timeout: 15)
|
||||
// Wait for login to complete
|
||||
sleep(3)
|
||||
}
|
||||
|
||||
/// Ensures the user is logged out before running a test.
|
||||
/// Ensures the user is logged out before running a test
|
||||
/// - Parameter app: The XCUIApplication instance
|
||||
static func ensureLoggedOut(app: XCUIApplication) {
|
||||
sleep(1)
|
||||
logout(app: app)
|
||||
ensureOnLoginScreen(app: app)
|
||||
}
|
||||
|
||||
/// Ensures the user is logged in with test credentials before running a test.
|
||||
static func ensureLoggedIn(
|
||||
app: XCUIApplication,
|
||||
username: String = "testuser",
|
||||
password: String = "TestPass123!"
|
||||
) {
|
||||
/// Ensures the user is logged in with test credentials before running a test
|
||||
/// - Parameter app: The XCUIApplication instance
|
||||
/// - Parameter username: Optional username (defaults to "testuser")
|
||||
/// - Parameter password: Optional password (defaults to "TestPass123!")
|
||||
static func ensureLoggedIn(app: XCUIApplication, username: String = "testuser", password: String = "TestPass123!") {
|
||||
sleep(1)
|
||||
|
||||
// Check if already logged in (tab bar visible)
|
||||
let tabBar = app.tabBars.firstMatch
|
||||
if tabBar.waitForExistence(timeout: 5) {
|
||||
return
|
||||
if tabBar.exists {
|
||||
return // Already logged in
|
||||
}
|
||||
|
||||
// Check if on login screen
|
||||
ensureOnLoginScreen(app: app)
|
||||
|
||||
let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
|
||||
if usernameField.waitForExistence(timeout: 5) {
|
||||
login(app: app, username: username, password: password)
|
||||
_ = tabBar.waitForExistence(timeout: 15)
|
||||
|
||||
// Wait for main screen to appear
|
||||
_ = tabBar.waitForExistence(timeout: 10)
|
||||
}
|
||||
}
|
||||
|
||||
static func ensureOnLoginScreen(app: XCUIApplication) {
|
||||
let usernameField = loginUsernameField(app: app)
|
||||
if usernameField.waitForExistence(timeout: 2) {
|
||||
return
|
||||
}
|
||||
|
||||
// Handle persisted authenticated sessions first.
|
||||
let mainTabsRoot = app.otherElements[UITestID.Root.mainTabs]
|
||||
if mainTabsRoot.exists || app.tabBars.firstMatch.exists {
|
||||
logout(app: app)
|
||||
if usernameField.waitForExistence(timeout: 8) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for a stable root state before interacting.
|
||||
let loginRoot = app.otherElements[UITestID.Root.login]
|
||||
let onboardingRoot = app.otherElements[UITestID.Root.onboarding]
|
||||
_ = loginRoot.waitForExistence(timeout: 5) || onboardingRoot.waitForExistence(timeout: 5)
|
||||
|
||||
if onboardingRoot.exists {
|
||||
// Handle both pure onboarding and onboarding + login sheet.
|
||||
let onboardingLoginButton = app.buttons[AccessibilityIdentifiers.Onboarding.loginButton]
|
||||
if onboardingLoginButton.waitForExistence(timeout: 5) {
|
||||
if onboardingLoginButton.isHittable {
|
||||
onboardingLoginButton.tap()
|
||||
} else {
|
||||
onboardingLoginButton.forceTap()
|
||||
}
|
||||
} else {
|
||||
let welcome = OnboardingWelcomeScreen(app: app)
|
||||
welcome.waitForLoad()
|
||||
welcome.tapAlreadyHaveAccount()
|
||||
}
|
||||
}
|
||||
|
||||
XCTAssertTrue(
|
||||
usernameField.waitForExistence(timeout: 20),
|
||||
"Expected to reach login screen from current app state"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
24
iosApp/XCUITest-Authoring.md
Normal file
24
iosApp/XCUITest-Authoring.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# XCUITest Authoring
|
||||
|
||||
## Required Architecture
|
||||
- Put shared test infrastructure in `/Users/treyt/Desktop/code/MyCribKMM/iosApp/CaseraUITests/Framework`.
|
||||
- Put feature suites in `/Users/treyt/Desktop/code/MyCribKMM/iosApp/CaseraUITests/Tests`.
|
||||
- Every test suite inherits `BaseUITestCase`.
|
||||
- Reusable multi-step setup belongs in `TestFlows`.
|
||||
- UI interactions should go through screen objects in `ScreenObjects.swift`.
|
||||
|
||||
## Runtime Contract
|
||||
- Launch args are standardized in `BaseUITestCase`:
|
||||
- `--ui-testing`
|
||||
- `--disable-animations`
|
||||
- `--reset-state`
|
||||
- App-side behavior for UI test mode is implemented in `/Users/treyt/Desktop/code/MyCribKMM/iosApp/iosApp/Helpers/UITestRuntime.swift`.
|
||||
|
||||
## Naming
|
||||
- Test method naming format: `test<CaseID>_<BehaviorDescription>()`.
|
||||
- Case IDs should stay stable once committed.
|
||||
|
||||
## Waiting and Flake Rules
|
||||
- Use helper waits from `BaseUITestCase` extensions.
|
||||
- Do not add blind `sleep()`.
|
||||
- Prefer stable accessibility identifiers over visible text selectors.
|
||||
16
iosApp/XCUITestSuiteTemplate.swift
Normal file
16
iosApp/XCUITestSuiteTemplate.swift
Normal file
@@ -0,0 +1,16 @@
|
||||
import XCTest
|
||||
|
||||
final class TemplateFeatureTests: BaseUITestCase {
|
||||
func testF900_TemplateBehavior() {
|
||||
// Arrange
|
||||
let root = RootScreen(app: app)
|
||||
root.waitForReady(timeout: defaultTimeout)
|
||||
|
||||
// Act
|
||||
let welcome = OnboardingWelcomeScreen(app: app)
|
||||
welcome.waitForLoad(timeout: defaultTimeout)
|
||||
|
||||
// Assert
|
||||
XCTAssertTrue(app.buttons[UITestID.Onboarding.startFreshButton].exists)
|
||||
}
|
||||
}
|
||||
49
iosApp/iosApp/Helpers/UITestRuntime.swift
Normal file
49
iosApp/iosApp/Helpers/UITestRuntime.swift
Normal file
@@ -0,0 +1,49 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import ComposeApp
|
||||
|
||||
/// Runtime contract between the app and XCUITests.
|
||||
enum UITestRuntime {
|
||||
static let uiTestingFlag = "--ui-testing"
|
||||
static let disableAnimationsFlag = "--disable-animations"
|
||||
static let resetStateFlag = "--reset-state"
|
||||
static let mockAuthFlag = "--ui-test-mock-auth"
|
||||
|
||||
static var launchArguments: [String] {
|
||||
ProcessInfo.processInfo.arguments
|
||||
}
|
||||
|
||||
static var isEnabled: Bool {
|
||||
launchArguments.contains(uiTestingFlag)
|
||||
}
|
||||
|
||||
static var shouldDisableAnimations: Bool {
|
||||
isEnabled && launchArguments.contains(disableAnimationsFlag)
|
||||
}
|
||||
|
||||
static var shouldResetState: Bool {
|
||||
isEnabled && launchArguments.contains(resetStateFlag)
|
||||
}
|
||||
|
||||
static var shouldMockAuth: Bool {
|
||||
isEnabled && launchArguments.contains(mockAuthFlag)
|
||||
}
|
||||
|
||||
static func configureForLaunch() {
|
||||
guard isEnabled else { return }
|
||||
|
||||
if shouldDisableAnimations {
|
||||
UIView.setAnimationsEnabled(false)
|
||||
}
|
||||
|
||||
UserDefaults.standard.set(true, forKey: "ui_testing_mode")
|
||||
}
|
||||
|
||||
static func resetStateIfRequested() {
|
||||
guard shouldResetState else { return }
|
||||
|
||||
DataManager.shared.clear()
|
||||
OnboardingState.shared.reset()
|
||||
ThemeManager.shared.currentTheme = .bright
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,19 @@ class LoginViewModel: ObservableObject {
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
|
||||
if UITestRuntime.shouldMockAuth {
|
||||
// Deterministic UI-test auth path scoped behind launch args.
|
||||
if username == "testuser" && password == "TestPass123!" {
|
||||
isVerified = true
|
||||
isLoading = false
|
||||
onLoginSuccess?(true)
|
||||
} else {
|
||||
isLoading = false
|
||||
errorMessage = "Invalid username or password"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
do {
|
||||
let result = try await APILayer.shared.login(
|
||||
|
||||
@@ -195,6 +195,7 @@ struct OnboardingCoordinator: View {
|
||||
.font(.title2)
|
||||
.foregroundColor(Color.appPrimary)
|
||||
}
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.backButton)
|
||||
.opacity(showBackButton ? 1 : 0)
|
||||
.disabled(!showBackButton)
|
||||
|
||||
@@ -203,6 +204,7 @@ struct OnboardingCoordinator: View {
|
||||
// Progress indicator
|
||||
if showProgressIndicator {
|
||||
OnboardingProgressIndicator(currentStep: currentProgressStep, totalSteps: 5)
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.progressIndicator)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
@@ -214,6 +216,7 @@ struct OnboardingCoordinator: View {
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.skipButton)
|
||||
.opacity(showSkipButton ? 1 : 0)
|
||||
.disabled(!showSkipButton)
|
||||
}
|
||||
|
||||
@@ -210,7 +210,8 @@ struct OnboardingCreateAccountContent: View {
|
||||
icon: "person.fill",
|
||||
placeholder: "Username",
|
||||
text: $viewModel.username,
|
||||
isFocused: focusedField == .username
|
||||
isFocused: focusedField == .username,
|
||||
accessibilityIdentifier: AccessibilityIdentifiers.Onboarding.usernameField
|
||||
)
|
||||
.focused($focusedField, equals: .username)
|
||||
.textInputAutocapitalization(.never)
|
||||
@@ -221,7 +222,8 @@ struct OnboardingCreateAccountContent: View {
|
||||
icon: "envelope.fill",
|
||||
placeholder: "Email",
|
||||
text: $viewModel.email,
|
||||
isFocused: focusedField == .email
|
||||
isFocused: focusedField == .email,
|
||||
accessibilityIdentifier: AccessibilityIdentifiers.Onboarding.emailField
|
||||
)
|
||||
.focused($focusedField, equals: .email)
|
||||
.textInputAutocapitalization(.never)
|
||||
@@ -233,7 +235,8 @@ struct OnboardingCreateAccountContent: View {
|
||||
icon: "lock.fill",
|
||||
placeholder: "Password",
|
||||
text: $viewModel.password,
|
||||
isFocused: focusedField == .password
|
||||
isFocused: focusedField == .password,
|
||||
accessibilityIdentifier: AccessibilityIdentifiers.Onboarding.passwordField
|
||||
)
|
||||
.focused($focusedField, equals: .password)
|
||||
|
||||
@@ -241,7 +244,8 @@ struct OnboardingCreateAccountContent: View {
|
||||
icon: "lock.fill",
|
||||
placeholder: "Confirm Password",
|
||||
text: $viewModel.confirmPassword,
|
||||
isFocused: focusedField == .confirmPassword
|
||||
isFocused: focusedField == .confirmPassword,
|
||||
accessibilityIdentifier: AccessibilityIdentifiers.Onboarding.confirmPasswordField
|
||||
)
|
||||
.focused($focusedField, equals: .confirmPassword)
|
||||
}
|
||||
@@ -359,6 +363,7 @@ private struct OrganicOnboardingTextField: View {
|
||||
let placeholder: String
|
||||
@Binding var text: String
|
||||
var isFocused: Bool = false
|
||||
var accessibilityIdentifier: String? = nil
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 14) {
|
||||
@@ -374,6 +379,7 @@ private struct OrganicOnboardingTextField: View {
|
||||
|
||||
TextField(placeholder, text: $text)
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.accessibilityIdentifier(accessibilityIdentifier ?? "")
|
||||
}
|
||||
.padding(14)
|
||||
.background(Color.appBackgroundPrimary.opacity(0.5))
|
||||
@@ -392,6 +398,7 @@ private struct OrganicOnboardingSecureField: View {
|
||||
let placeholder: String
|
||||
@Binding var text: String
|
||||
var isFocused: Bool = false
|
||||
var accessibilityIdentifier: String? = nil
|
||||
@State private var showPassword = false
|
||||
|
||||
var body: some View {
|
||||
@@ -410,10 +417,12 @@ private struct OrganicOnboardingSecureField: View {
|
||||
TextField(placeholder, text: $text)
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.textContentType(.password)
|
||||
.accessibilityIdentifier(accessibilityIdentifier ?? "")
|
||||
} else {
|
||||
SecureField(placeholder, text: $text)
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.textContentType(.password)
|
||||
.accessibilityIdentifier(accessibilityIdentifier ?? "")
|
||||
}
|
||||
|
||||
Button(action: { showPassword.toggle() }) {
|
||||
|
||||
@@ -68,6 +68,7 @@ struct OnboardingValuePropsContent: View {
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.valuePropsTitle)
|
||||
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||
.frame(maxHeight: .infinity)
|
||||
|
||||
@@ -104,6 +105,7 @@ struct OnboardingValuePropsContent: View {
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
|
||||
.naturalShadow(.medium)
|
||||
}
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.valuePropsNextButton)
|
||||
.padding(.horizontal, OrganicSpacing.comfortable)
|
||||
.padding(.bottom, OrganicSpacing.airy)
|
||||
}
|
||||
|
||||
@@ -177,6 +177,11 @@ struct OnboardingWelcomeView: View {
|
||||
.opacity(0.5)
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
|
||||
// Deterministic marker for UI tests.
|
||||
Color.clear
|
||||
.frame(width: 1, height: 1)
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.welcomeTitle)
|
||||
}
|
||||
.sheet(isPresented: $showingLoginSheet) {
|
||||
LoginView(onLoginSuccess: {
|
||||
|
||||
@@ -7,6 +7,9 @@ import Combine
|
||||
/// Kicks off API calls that update DataManager, letting views react to cache updates.
|
||||
@MainActor
|
||||
class ResidenceViewModel: ObservableObject {
|
||||
private static var uiTestMockResidences: [ResidenceResponse] = []
|
||||
private static var uiTestNextResidenceId: Int = 1000
|
||||
|
||||
// MARK: - Published Properties (from DataManager observation)
|
||||
@Published var myResidences: MyResidencesResponse?
|
||||
@Published var residences: [ResidenceResponse] = []
|
||||
@@ -93,6 +96,18 @@ class ResidenceViewModel: ObservableObject {
|
||||
|
||||
/// Load my residences - checks cache first, then fetches if needed
|
||||
func loadMyResidences(forceRefresh: Bool = false) {
|
||||
if UITestRuntime.shouldMockAuth {
|
||||
if Self.uiTestMockResidences.isEmpty || forceRefresh {
|
||||
if Self.uiTestMockResidences.isEmpty {
|
||||
Self.uiTestMockResidences = [makeMockResidence(name: "Seed Residence")]
|
||||
}
|
||||
}
|
||||
myResidences = MyResidencesResponse(residences: Self.uiTestMockResidences)
|
||||
isLoading = false
|
||||
errorMessage = nil
|
||||
return
|
||||
}
|
||||
|
||||
errorMessage = nil
|
||||
|
||||
// Check if we have cached data and don't need to refresh
|
||||
@@ -122,6 +137,13 @@ class ResidenceViewModel: ObservableObject {
|
||||
}
|
||||
|
||||
func getResidence(id: Int32) {
|
||||
if UITestRuntime.shouldMockAuth {
|
||||
selectedResidence = Self.uiTestMockResidences.first(where: { $0.id == id })
|
||||
isLoading = false
|
||||
errorMessage = selectedResidence == nil ? "Residence not found" : nil
|
||||
return
|
||||
}
|
||||
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
|
||||
@@ -154,6 +176,22 @@ class ResidenceViewModel: ObservableObject {
|
||||
|
||||
/// Creates a residence and returns the created residence on success
|
||||
func createResidence(request: ResidenceCreateRequest, completion: @escaping (ResidenceResponse?) -> Void) {
|
||||
if UITestRuntime.shouldMockAuth {
|
||||
let residence = makeMockResidence(
|
||||
name: request.name,
|
||||
streetAddress: request.streetAddress ?? "",
|
||||
city: request.city ?? "",
|
||||
stateProvince: request.stateProvince ?? "",
|
||||
postalCode: request.postalCode ?? ""
|
||||
)
|
||||
Self.uiTestMockResidences.append(residence)
|
||||
myResidences = MyResidencesResponse(residences: Self.uiTestMockResidences)
|
||||
isLoading = false
|
||||
errorMessage = nil
|
||||
completion(residence)
|
||||
return
|
||||
}
|
||||
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
|
||||
@@ -293,4 +331,44 @@ class ResidenceViewModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func makeMockResidence(
|
||||
name: String,
|
||||
streetAddress: String = "",
|
||||
city: String = "",
|
||||
stateProvince: String = "",
|
||||
postalCode: String = ""
|
||||
) -> ResidenceResponse {
|
||||
let id = Self.uiTestNextResidenceId
|
||||
Self.uiTestNextResidenceId += 1
|
||||
let now = "2026-02-20T00:00:00Z"
|
||||
return ResidenceResponse(
|
||||
id: Int32(id),
|
||||
ownerId: 1,
|
||||
owner: ResidenceUserResponse(id: 1, username: "testuser", email: "test@example.com", firstName: "UI", lastName: "Tester"),
|
||||
users: [],
|
||||
name: name,
|
||||
propertyTypeId: 1,
|
||||
propertyType: ResidenceType(id: 1, name: "House"),
|
||||
streetAddress: streetAddress,
|
||||
apartmentUnit: "",
|
||||
city: city,
|
||||
stateProvince: stateProvince,
|
||||
postalCode: postalCode,
|
||||
country: "USA",
|
||||
bedrooms: nil,
|
||||
bathrooms: nil,
|
||||
squareFootage: nil,
|
||||
lotSize: nil,
|
||||
yearBuilt: nil,
|
||||
description: "",
|
||||
purchaseDate: nil,
|
||||
purchasePrice: nil,
|
||||
isPrimary: false,
|
||||
isActive: true,
|
||||
overdueCount: 0,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,13 @@ class AuthenticationManager: ObservableObject {
|
||||
}
|
||||
|
||||
func checkAuthenticationStatus() {
|
||||
if UITestRuntime.isEnabled {
|
||||
isAuthenticated = DataManager.shared.isAuthenticated()
|
||||
isVerified = isAuthenticated
|
||||
isCheckingAuth = false
|
||||
return
|
||||
}
|
||||
|
||||
isCheckingAuth = true
|
||||
|
||||
// Check if token exists via DataManager (single source of truth)
|
||||
@@ -29,11 +36,6 @@ class AuthenticationManager: ObservableObject {
|
||||
// Fetch current user and initialize lookups immediately for all authenticated users
|
||||
Task { @MainActor in
|
||||
do {
|
||||
// Prefer cached user state when available to avoid blocking on transient failures.
|
||||
if let cachedUser = DataManagerObservable.shared.currentUser {
|
||||
self.isVerified = cachedUser.verified
|
||||
}
|
||||
|
||||
// Initialize lookups right away for any authenticated user
|
||||
// This fetches /static_data/ and /upgrade-triggers/ at app start
|
||||
print("🚀 Initializing lookups at app start...")
|
||||
@@ -52,22 +54,18 @@ class AuthenticationManager: ObservableObject {
|
||||
if self.isVerified {
|
||||
await StoreKitManager.shared.verifyEntitlementsOnLaunch()
|
||||
}
|
||||
} else if let error = ApiResultBridge.error(from: result) {
|
||||
if self.shouldForceLogout(for: error) {
|
||||
DataManager.shared.clear()
|
||||
SubscriptionCacheWrapper.shared.clear()
|
||||
PushNotificationManager.shared.clearRegistrationCache()
|
||||
self.isAuthenticated = false
|
||||
self.isVerified = false
|
||||
} else {
|
||||
print("⚠️ Auth status check failed but session kept: \(error.message)")
|
||||
}
|
||||
} else {
|
||||
print("⚠️ Auth status check returned unexpected result type: \(type(of: result))")
|
||||
} else if result is ApiResultError {
|
||||
// Token is invalid, clear all data via DataManager
|
||||
DataManager.shared.clear()
|
||||
self.isAuthenticated = false
|
||||
self.isVerified = false
|
||||
}
|
||||
} catch {
|
||||
print("❌ Failed to check auth status: \(error)")
|
||||
// Keep session on transient failures and preserve last-known verification state.
|
||||
// On error, assume token is invalid
|
||||
DataManager.shared.clear()
|
||||
self.isAuthenticated = false
|
||||
self.isVerified = false
|
||||
}
|
||||
|
||||
self.isCheckingAuth = false
|
||||
@@ -78,6 +76,8 @@ class AuthenticationManager: ObservableObject {
|
||||
isAuthenticated = true
|
||||
isVerified = verified
|
||||
|
||||
guard !UITestRuntime.isEnabled else { return }
|
||||
|
||||
// Register device for push notifications now that user is authenticated
|
||||
PushNotificationManager.shared.registerDeviceAfterLogin()
|
||||
}
|
||||
@@ -85,6 +85,8 @@ class AuthenticationManager: ObservableObject {
|
||||
func markVerified() {
|
||||
isVerified = true
|
||||
|
||||
guard !UITestRuntime.isEnabled else { return }
|
||||
|
||||
// Lookups are already initialized at app start or during login/register
|
||||
// Just verify subscription entitlements after user becomes verified
|
||||
Task {
|
||||
@@ -99,9 +101,6 @@ class AuthenticationManager: ObservableObject {
|
||||
_ = try? await APILayer.shared.logout()
|
||||
}
|
||||
|
||||
SubscriptionCacheWrapper.shared.clear()
|
||||
PushNotificationManager.shared.clearRegistrationCache()
|
||||
|
||||
// Clear widget data (tasks and auth token)
|
||||
WidgetDataManager.shared.clearCache()
|
||||
WidgetDataManager.shared.clearAuthToken()
|
||||
@@ -120,64 +119,69 @@ class AuthenticationManager: ObservableObject {
|
||||
func resetOnboarding() {
|
||||
OnboardingState.shared.reset()
|
||||
}
|
||||
|
||||
private func shouldForceLogout(for error: ApiResultError) -> Bool {
|
||||
if let statusCode = error.code?.intValue, statusCode == 401 || statusCode == 403 {
|
||||
return true
|
||||
}
|
||||
|
||||
let message = error.message.lowercased()
|
||||
return message.contains("error.invalid_token")
|
||||
|| message.contains("error.not_authenticated")
|
||||
|| message.contains("not authenticated")
|
||||
}
|
||||
}
|
||||
|
||||
/// Root view that handles authentication flow: loading -> onboarding -> login -> verify email -> main app
|
||||
struct RootView: View {
|
||||
@Binding private var resetToken: String?
|
||||
@EnvironmentObject private var themeManager: ThemeManager
|
||||
@StateObject private var authManager = AuthenticationManager.shared
|
||||
@StateObject private var onboardingState = OnboardingState.shared
|
||||
@State private var refreshID = UUID()
|
||||
|
||||
init(resetToken: Binding<String?> = .constant(nil)) {
|
||||
self._resetToken = resetToken
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if authManager.isCheckingAuth {
|
||||
// Show loading while checking auth status
|
||||
loadingView
|
||||
} else if !onboardingState.hasCompletedOnboarding {
|
||||
// Show onboarding for first-time users (includes auth + verification steps)
|
||||
// This takes precedence because we need to finish the onboarding flow
|
||||
OnboardingCoordinator(onComplete: {
|
||||
// Onboarding complete - mark verified and refresh the view
|
||||
authManager.markVerified()
|
||||
refreshID = UUID()
|
||||
})
|
||||
} else if !authManager.isAuthenticated {
|
||||
// Show login screen for returning users
|
||||
LoginView(resetToken: $resetToken)
|
||||
} else if !authManager.isVerified {
|
||||
// Show email verification screen (for returning users who haven't verified)
|
||||
VerifyEmailView(
|
||||
onVerifySuccess: {
|
||||
authManager.markVerified()
|
||||
},
|
||||
onLogout: {
|
||||
authManager.logout()
|
||||
ZStack(alignment: .topLeading) {
|
||||
Group {
|
||||
if authManager.isCheckingAuth {
|
||||
// Show loading while checking auth status
|
||||
loadingView
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Common.loadingIndicator)
|
||||
} else if !onboardingState.hasCompletedOnboarding {
|
||||
// Show onboarding for first-time users (includes auth + verification steps)
|
||||
// This takes precedence because we need to finish the onboarding flow
|
||||
ZStack(alignment: .topLeading) {
|
||||
OnboardingCoordinator(onComplete: {
|
||||
// Onboarding complete - mark verified and refresh the view
|
||||
authManager.markVerified()
|
||||
refreshID = UUID()
|
||||
})
|
||||
Color.clear
|
||||
.frame(width: 1, height: 1)
|
||||
.accessibilityIdentifier("ui.root.onboarding")
|
||||
}
|
||||
)
|
||||
} else {
|
||||
// Show main app
|
||||
MainTabView(refreshID: refreshID)
|
||||
.onChange(of: themeManager.currentTheme) { _ in
|
||||
refreshID = UUID()
|
||||
} else if !authManager.isAuthenticated {
|
||||
// Show login screen for returning users
|
||||
ZStack(alignment: .topLeading) {
|
||||
LoginView()
|
||||
Color.clear
|
||||
.frame(width: 1, height: 1)
|
||||
.accessibilityIdentifier("ui.root.login")
|
||||
}
|
||||
} else if !authManager.isVerified {
|
||||
// Show email verification screen (for returning users who haven't verified)
|
||||
VerifyEmailView(
|
||||
onVerifySuccess: {
|
||||
authManager.markVerified()
|
||||
},
|
||||
onLogout: {
|
||||
authManager.logout()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
// Show main app
|
||||
ZStack(alignment: .topLeading) {
|
||||
MainTabView(refreshID: refreshID)
|
||||
.onChange(of: themeManager.currentTheme) { _ in
|
||||
refreshID = UUID()
|
||||
}
|
||||
Color.clear
|
||||
.frame(width: 1, height: 1)
|
||||
.accessibilityIdentifier("ui.root.mainTabs")
|
||||
}
|
||||
}
|
||||
}
|
||||
Color.clear
|
||||
.frame(width: 1, height: 1)
|
||||
.accessibilityIdentifier("ui.app.ready")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -332,6 +332,7 @@ struct SecureIconTextField: View {
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.passwordVisibilityToggle)
|
||||
}
|
||||
.padding(16)
|
||||
.background(Color.appBackgroundPrimary.opacity(0.5))
|
||||
|
||||
@@ -21,9 +21,7 @@ struct iOSApp: App {
|
||||
}
|
||||
|
||||
init() {
|
||||
// Set up Keychain delegate BEFORE DataManager initialization
|
||||
// so token reads/writes use Keychain instead of NSUserDefaults
|
||||
TokenManager.Companion.shared.keychainDelegate = KeychainHelper.shared
|
||||
UITestRuntime.configureForLaunch()
|
||||
|
||||
// Initialize DataManager with platform-specific managers
|
||||
// This must be done before any other operations that access DataManager
|
||||
@@ -33,24 +31,34 @@ struct iOSApp: App {
|
||||
persistenceMgr: PersistenceManager()
|
||||
)
|
||||
|
||||
if UITestRuntime.isEnabled {
|
||||
Task { @MainActor in
|
||||
UITestRuntime.resetStateIfRequested()
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize TokenStorage once at app startup (legacy support)
|
||||
TokenStorage.shared.initialize(manager: TokenManager.Companion.shared.getInstance())
|
||||
|
||||
// Initialize PostHog Analytics
|
||||
AnalyticsManager.shared.configure()
|
||||
if !UITestRuntime.isEnabled {
|
||||
// Initialize PostHog Analytics
|
||||
PostHogAnalytics.shared.initialize()
|
||||
}
|
||||
|
||||
// Initialize lookups at app start (public endpoints, no auth required)
|
||||
// This fetches /static_data/ and /upgrade-triggers/ immediately
|
||||
Task {
|
||||
print("🚀 Initializing lookups at app start...")
|
||||
_ = try? await APILayer.shared.initializeLookups()
|
||||
print("✅ Lookups initialized")
|
||||
if !UITestRuntime.isEnabled {
|
||||
Task {
|
||||
print("🚀 Initializing lookups at app start...")
|
||||
_ = try? await APILayer.shared.initializeLookups()
|
||||
print("✅ Lookups initialized")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
RootView(resetToken: $deepLinkResetToken)
|
||||
RootView()
|
||||
.environmentObject(themeManager)
|
||||
.environmentObject(contractorSharingManager)
|
||||
.environmentObject(residenceSharingManager)
|
||||
@@ -58,10 +66,9 @@ struct iOSApp: App {
|
||||
handleIncomingURL(url: url)
|
||||
}
|
||||
.onChange(of: scenePhase) { newPhase in
|
||||
if newPhase == .active {
|
||||
// Refresh analytics super properties (subscription, settings may have changed)
|
||||
AnalyticsManager.shared.updateSuperProperties()
|
||||
guard !UITestRuntime.isEnabled else { return }
|
||||
|
||||
if newPhase == .active {
|
||||
// Sync auth token to widget if user is logged in
|
||||
// This ensures widget has credentials even if user logged in before widget support was added
|
||||
if let token = TokenStorage.shared.getToken() {
|
||||
@@ -97,9 +104,6 @@ struct iOSApp: App {
|
||||
}
|
||||
}
|
||||
} else if newPhase == .background {
|
||||
// Flush pending analytics events before app suspends
|
||||
AnalyticsManager.shared.flush()
|
||||
|
||||
// Refresh widget when app goes to background
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
|
||||
@@ -194,7 +198,7 @@ struct iOSApp: App {
|
||||
|
||||
/// Handles all incoming URLs - both deep links and file opens
|
||||
private func handleIncomingURL(url: URL) {
|
||||
print("URL received with scheme: \(url.scheme ?? "unknown")")
|
||||
print("URL received: \(url)")
|
||||
|
||||
// Handle .casera file imports
|
||||
if url.pathExtension.lowercased() == "casera" {
|
||||
@@ -208,7 +212,7 @@ struct iOSApp: App {
|
||||
return
|
||||
}
|
||||
|
||||
print("Unrecognized URL scheme: \(url.scheme ?? "unknown")")
|
||||
print("Unrecognized URL: \(url)")
|
||||
}
|
||||
|
||||
/// Handles .casera file imports - detects type and routes accordingly
|
||||
@@ -265,7 +269,7 @@ struct iOSApp: App {
|
||||
if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
||||
let queryItems = components.queryItems,
|
||||
let token = queryItems.first(where: { $0.name == "token" })?.value {
|
||||
print("Password reset deep link received")
|
||||
print("Reset token extracted: \(token)")
|
||||
deepLinkResetToken = token
|
||||
} else {
|
||||
print("No token found in deep link")
|
||||
|
||||
Reference in New Issue
Block a user