Refactor iOS UI tests to blueprint architecture and migrate legacy suites

This commit is contained in:
treyt
2026-02-19 17:30:58 -06:00
parent 09be5fa444
commit 710a8bd1d6
36 changed files with 835 additions and 6263 deletions

View File

@@ -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,

View File

@@ -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

View File

@@ -1,272 +0,0 @@
import Foundation
/// Centralized accessibility identifiers for UI testing
/// These identifiers are used by XCUITests to locate and interact with UI elements
struct AccessibilityIdentifiers {
// MARK: - Authentication
struct Authentication {
static let usernameField = "Login.UsernameField"
static let passwordField = "Login.PasswordField"
static let loginButton = "Login.LoginButton"
static let signUpButton = "Login.SignUpButton"
static let forgotPasswordButton = "Login.ForgotPasswordButton"
static let passwordVisibilityToggle = "Login.PasswordVisibilityToggle"
static let appleSignInButton = "Login.AppleSignInButton"
// Registration
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"
// Verification
static let verificationCodeField = "Verification.CodeField"
static let verifyButton = "Verification.VerifyButton"
static let resendCodeButton = "Verification.ResendButton"
}
// MARK: - Navigation
struct Navigation {
static let residencesTab = "TabBar.Residences"
static let tasksTab = "TabBar.Tasks"
static let contractorsTab = "TabBar.Contractors"
static let documentsTab = "TabBar.Documents"
static let profileTab = "TabBar.Profile"
static let backButton = "Navigation.BackButton"
}
// MARK: - Residence
struct Residence {
// List
static let addButton = "Residence.AddButton"
static let residencesList = "Residence.List"
static let residenceCard = "Residence.Card"
static let emptyStateView = "Residence.EmptyState"
static let emptyStateButton = "Residence.EmptyState.AddButton"
// Form
static let nameField = "ResidenceForm.NameField"
static let propertyTypePicker = "ResidenceForm.PropertyTypePicker"
static let streetAddressField = "ResidenceForm.StreetAddressField"
static let apartmentUnitField = "ResidenceForm.ApartmentUnitField"
static let cityField = "ResidenceForm.CityField"
static let stateProvinceField = "ResidenceForm.StateProvinceField"
static let postalCodeField = "ResidenceForm.PostalCodeField"
static let countryField = "ResidenceForm.CountryField"
static let bedroomsField = "ResidenceForm.BedroomsField"
static let bathroomsField = "ResidenceForm.BathroomsField"
static let squareFootageField = "ResidenceForm.SquareFootageField"
static let lotSizeField = "ResidenceForm.LotSizeField"
static let yearBuiltField = "ResidenceForm.YearBuiltField"
static let descriptionField = "ResidenceForm.DescriptionField"
static let isPrimaryToggle = "ResidenceForm.IsPrimaryToggle"
static let saveButton = "ResidenceForm.SaveButton"
static let formCancelButton = "ResidenceForm.CancelButton"
// Detail
static let detailView = "ResidenceDetail.View"
static let editButton = "ResidenceDetail.EditButton"
static let deleteButton = "ResidenceDetail.DeleteButton"
static let shareButton = "ResidenceDetail.ShareButton"
static let manageUsersButton = "ResidenceDetail.ManageUsersButton"
static let tasksSection = "ResidenceDetail.TasksSection"
static let addTaskButton = "ResidenceDetail.AddTaskButton"
}
// MARK: - Task
struct Task {
// List/Kanban
static let addButton = "Task.AddButton"
static let tasksList = "Task.List"
static let taskCard = "Task.Card"
static let emptyStateView = "Task.EmptyState"
static let kanbanView = "Task.KanbanView"
static let overdueColumn = "Task.Column.Overdue"
static let upcomingColumn = "Task.Column.Upcoming"
static let inProgressColumn = "Task.Column.InProgress"
static let completedColumn = "Task.Column.Completed"
// Form
static let titleField = "TaskForm.TitleField"
static let descriptionField = "TaskForm.DescriptionField"
static let categoryPicker = "TaskForm.CategoryPicker"
static let frequencyPicker = "TaskForm.FrequencyPicker"
static let priorityPicker = "TaskForm.PriorityPicker"
static let statusPicker = "TaskForm.StatusPicker"
static let dueDatePicker = "TaskForm.DueDatePicker"
static let intervalDaysField = "TaskForm.IntervalDaysField"
static let estimatedCostField = "TaskForm.EstimatedCostField"
static let residencePicker = "TaskForm.ResidencePicker"
static let saveButton = "TaskForm.SaveButton"
static let formCancelButton = "TaskForm.CancelButton"
// Detail
static let detailView = "TaskDetail.View"
static let editButton = "TaskDetail.EditButton"
static let deleteButton = "TaskDetail.DeleteButton"
static let markInProgressButton = "TaskDetail.MarkInProgressButton"
static let completeButton = "TaskDetail.CompleteButton"
static let detailCancelButton = "TaskDetail.CancelButton"
// Completion
static let completionDatePicker = "TaskCompletion.CompletionDatePicker"
static let actualCostField = "TaskCompletion.ActualCostField"
static let ratingView = "TaskCompletion.RatingView"
static let notesField = "TaskCompletion.NotesField"
static let photosPicker = "TaskCompletion.PhotosPicker"
static let submitButton = "TaskCompletion.SubmitButton"
}
// MARK: - Contractor
struct Contractor {
static let addButton = "Contractor.AddButton"
static let contractorsList = "Contractor.List"
static let contractorCard = "Contractor.Card"
static let emptyStateView = "Contractor.EmptyState"
// Form
static let nameField = "ContractorForm.NameField"
static let companyField = "ContractorForm.CompanyField"
static let emailField = "ContractorForm.EmailField"
static let phoneField = "ContractorForm.PhoneField"
static let specialtyPicker = "ContractorForm.SpecialtyPicker"
static let ratingView = "ContractorForm.RatingView"
static let notesField = "ContractorForm.NotesField"
static let saveButton = "ContractorForm.SaveButton"
static let formCancelButton = "ContractorForm.CancelButton"
// Detail
static let detailView = "ContractorDetail.View"
static let editButton = "ContractorDetail.EditButton"
static let deleteButton = "ContractorDetail.DeleteButton"
static let callButton = "ContractorDetail.CallButton"
static let emailButton = "ContractorDetail.EmailButton"
}
// MARK: - Document
struct Document {
static let addButton = "Document.AddButton"
static let documentsList = "Document.List"
static let documentCard = "Document.Card"
static let emptyStateView = "Document.EmptyState"
// Form
static let titleField = "DocumentForm.TitleField"
static let typePicker = "DocumentForm.TypePicker"
static let categoryPicker = "DocumentForm.CategoryPicker"
static let residencePicker = "DocumentForm.ResidencePicker"
static let filePicker = "DocumentForm.FilePicker"
static let notesField = "DocumentForm.NotesField"
static let expirationDatePicker = "DocumentForm.ExpirationDatePicker"
static let saveButton = "DocumentForm.SaveButton"
static let formCancelButton = "DocumentForm.CancelButton"
// Detail
static let detailView = "DocumentDetail.View"
static let editButton = "DocumentDetail.EditButton"
static let deleteButton = "DocumentDetail.DeleteButton"
static let shareButton = "DocumentDetail.ShareButton"
static let downloadButton = "DocumentDetail.DownloadButton"
}
// MARK: - Onboarding
struct Onboarding {
// Welcome Screen
static let welcomeTitle = "Onboarding.WelcomeTitle"
static let startFreshButton = "Onboarding.StartFreshButton"
static let joinExistingButton = "Onboarding.JoinExistingButton"
static let loginButton = "Onboarding.LoginButton"
// Value Props Screen
static let valuePropsTitle = "Onboarding.ValuePropsTitle"
static let valuePropsNextButton = "Onboarding.ValuePropsNextButton"
// Name Residence Screen
static let nameResidenceTitle = "Onboarding.NameResidenceTitle"
static let residenceNameField = "Onboarding.ResidenceNameField"
static let nameResidenceContinueButton = "Onboarding.NameResidenceContinueButton"
// Create Account Screen
static let createAccountTitle = "Onboarding.CreateAccountTitle"
static let appleSignInButton = "Onboarding.AppleSignInButton"
static let emailSignUpExpandButton = "Onboarding.EmailSignUpExpandButton"
static let usernameField = "Onboarding.UsernameField"
static let emailField = "Onboarding.EmailField"
static let passwordField = "Onboarding.PasswordField"
static let confirmPasswordField = "Onboarding.ConfirmPasswordField"
static let createAccountButton = "Onboarding.CreateAccountButton"
static let loginLinkButton = "Onboarding.LoginLinkButton"
// Verify Email Screen
static let verifyEmailTitle = "Onboarding.VerifyEmailTitle"
static let verificationCodeField = "Onboarding.VerificationCodeField"
static let verifyButton = "Onboarding.VerifyButton"
// Join Residence Screen
static let joinResidenceTitle = "Onboarding.JoinResidenceTitle"
static let shareCodeField = "Onboarding.ShareCodeField"
static let joinResidenceButton = "Onboarding.JoinResidenceButton"
// First Task Screen
static let firstTaskTitle = "Onboarding.FirstTaskTitle"
static let taskSelectionCounter = "Onboarding.TaskSelectionCounter"
static let addPopularTasksButton = "Onboarding.AddPopularTasksButton"
static let addTasksContinueButton = "Onboarding.AddTasksContinueButton"
static let taskCategorySection = "Onboarding.TaskCategorySection"
static let taskTemplateRow = "Onboarding.TaskTemplateRow"
// Subscription Screen
static let subscriptionTitle = "Onboarding.SubscriptionTitle"
static let yearlyPlanCard = "Onboarding.YearlyPlanCard"
static let monthlyPlanCard = "Onboarding.MonthlyPlanCard"
static let startTrialButton = "Onboarding.StartTrialButton"
static let continueWithFreeButton = "Onboarding.ContinueWithFreeButton"
// Navigation
static let backButton = "Onboarding.BackButton"
static let skipButton = "Onboarding.SkipButton"
static let progressIndicator = "Onboarding.ProgressIndicator"
}
// MARK: - Profile
struct Profile {
static let logoutButton = "Profile.LogoutButton"
static let editProfileButton = "Profile.EditProfileButton"
static let settingsButton = "Profile.SettingsButton"
static let notificationsToggle = "Profile.NotificationsToggle"
static let darkModeToggle = "Profile.DarkModeToggle"
static let aboutButton = "Profile.AboutButton"
static let helpButton = "Profile.HelpButton"
}
// MARK: - Alerts & Modals
struct Alert {
static let confirmButton = "Alert.ConfirmButton"
static let cancelButton = "Alert.CancelButton"
static let deleteButton = "Alert.DeleteButton"
static let okButton = "Alert.OKButton"
}
// MARK: - Common
struct Common {
static let loadingIndicator = "Common.LoadingIndicator"
static let errorView = "Common.ErrorView"
static let retryButton = "Common.RetryButton"
static let searchField = "Common.SearchField"
static let filterButton = "Common.FilterButton"
static let sortButton = "Common.SortButton"
static let refreshControl = "Common.RefreshControl"
}
}
// MARK: - Helper Extension
extension String {
/// Convenience method to generate dynamic identifiers
/// Example: "Residence.Card.\(residenceId)"
func withId(_ id: Any) -> String {
return "\(self).\(id)"
}
}

View File

@@ -0,0 +1,117 @@
import XCTest
class BaseUITestCase: XCTestCase {
let app = XCUIApplication()
let shortTimeout: TimeInterval = 5
let defaultTimeout: TimeInterval = 15
let longTimeout: TimeInterval = 30
override func setUpWithError() throws {
continueAfterFailure = false
XCUIDevice.shared.orientation = .portrait
app.launchArguments = [
"--ui-testing",
"--disable-animations",
"--reset-state"
]
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)
}
}

View File

@@ -0,0 +1,247 @@
import XCTest
struct UITestID {
struct Root {
static let ready = "ui.app.ready"
static let onboarding = "ui.root.onboarding"
static let login = "ui.root.login"
}
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.waitUntilHittable(timeout: 10).tap()
}
}
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) {
usernameField.waitForExistenceOrFail(timeout: 10)
usernameField.forceTap()
usernameField.typeText(username)
emailField.waitForExistenceOrFail(timeout: 10)
emailField.forceTap()
emailField.typeText(email)
passwordField.waitForExistenceOrFail(timeout: 10)
passwordField.forceTap()
passwordField.typeText(password)
confirmPasswordField.waitForExistenceOrFail(timeout: 10)
confirmPasswordField.forceTap()
confirmPasswordField.typeText(password)
}
func tapCancel() {
cancelButton.waitUntilHittable(timeout: 10).tap()
}
}

View File

@@ -0,0 +1,47 @@
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 = navigateToLoginFromOnboarding(app: app)
login.tapSignUp()
let register = RegisterScreen(app: app)
register.waitForLoad()
return register
}
}

View File

@@ -1,41 +0,0 @@
//
// CaseraUITests.swift
// CaseraUITests
//
// Created by Trey Tartt on 11/19/25.
//
import XCTest
final class CaseraUITests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
// In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false
// In UI tests it's important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
@MainActor
func testExample() throws {
// UI tests must launch the application that they test.
let app = XCUIApplication()
app.launch()
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
@MainActor
func testLaunchPerformance() throws {
// This measures how long it takes to launch your application.
measure(metrics: [XCTApplicationLaunchMetric()]) {
XCUIApplication().launch()
}
}
}

View File

@@ -1,33 +0,0 @@
//
// CaseraUITestsLaunchTests.swift
// CaseraUITests
//
// Created by Trey Tartt on 11/19/25.
//
import XCTest
final class CaseraUITestsLaunchTests: XCTestCase {
override class var runsForEachTargetApplicationUIConfiguration: Bool {
true
}
override func setUpWithError() throws {
continueAfterFailure = false
}
@MainActor
func testLaunch() throws {
let app = XCUIApplication()
app.launch()
// Insert steps here to perform after app launch but before taking a screenshot,
// such as logging into a test account or navigating somewhere in the app
let attachment = XCTAttachment(screenshot: app.screenshot())
attachment.name = "Launch Screen"
attachment.lifetime = .keepAlways
add(attachment)
}
}

View File

@@ -1,63 +1,8 @@
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: XCTestCase {
var app: XCUIApplication!
override func setUpWithError() throws {
continueAfterFailure = false
app = XCUIApplication()
app.launch()
// CRITICAL: Ensure we're logged out before each test
ensureLoggedOut()
}
override func tearDownWithError() throws {
app = nil
}
// 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.staticTexts["Welcome Back"]
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")
final class SimpleLoginTest: BaseUITestCase {
func testSimpleLoginEntryRenders() {
let login = TestFlows.navigateToLoginFromOnboarding(app: app)
login.waitForLoad(timeout: defaultTimeout)
}
}

View File

@@ -1,151 +1,8 @@
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: XCTestCase {
var app: XCUIApplication!
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
override func setUpWithError() throws {
continueAfterFailure = false
app = XCUIApplication()
app.launch()
sleep(2)
}
override func tearDownWithError() throws {
app.terminate()
app = nil
}
func test_onboarding() {
let app = XCUIApplication()
app.activate()
sleep(3)
let springboardApp = XCUIApplication(bundleIdentifier: "com.apple.springboard")
springboardApp/*@START_MENU_TOKEN@*/.buttons["Allow"]/*[[".otherElements.buttons[\"Allow\"]",".buttons[\"Allow\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap()
sleep(1)
app/*@START_MENU_TOKEN@*/.buttons["Onboarding.StartFreshButton"]/*[[".buttons",".containing(.staticText, identifier: \"Start Fresh\")",".containing(.image, identifier: \"icon\")",".otherElements",".buttons[\"Start Fresh\"]",".buttons[\"Onboarding.StartFreshButton\"]"],[[[-1,5],[-1,4],[-1,3,2],[-1,0,1]],[[-1,2],[-1,1]],[[-1,5],[-1,4]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap()
sleep(1)
app.cells/*@START_MENU_TOKEN@*/.firstMatch/*[[".containing(.other, identifier: nil).firstMatch",".firstMatch"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.swipeLeft()
sleep(1)
app/*@START_MENU_TOKEN@*/.staticTexts["Delayed maintenance turns $100 fixes into $10,000 disasters. Stay ahead of problems before they drain your wallet."]/*[[".otherElements.staticTexts[\"Delayed maintenance turns $100 fixes into $10,000 disasters. Stay ahead of problems before they drain your wallet.\"]",".staticTexts[\"Delayed maintenance turns $100 fixes into $10,000 disasters. Stay ahead of problems before they drain your wallet.\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.firstMatch.swipeLeft()
sleep(1)
app/*@START_MENU_TOKEN@*/.staticTexts["Snap a photo of your receipts and warranties. When something breaks, you'll know exactly what's covered—instantly."]/*[[".otherElements.staticTexts[\"Snap a photo of your receipts and warranties. When something breaks, you'll know exactly what's coveredinstantly.\"]",".staticTexts[\"Snap a photo of your receipts and warranties. When something breaks, you'll know exactly what's coveredinstantly.\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.firstMatch.swipeLeft()
sleep(1)
app/*@START_MENU_TOKEN@*/.staticTexts["I'm Ready!"]/*[[".buttons[\"I'm Ready!\"].staticTexts",".buttons.staticTexts[\"I'm Ready!\"]",".staticTexts[\"I'm Ready!\"]"],[[[-1,2],[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap()
sleep(1)
app/*@START_MENU_TOKEN@*/.textFields["Onboarding.ResidenceNameField"]/*[[".otherElements",".textFields[\"Xcuites\"]",".textFields[\"The Smith Residence\"]",".textFields[\"Onboarding.ResidenceNameField\"]",".textFields"],[[[-1,3],[-1,2],[-1,1],[-1,4],[-1,0,1]],[[-1,3],[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.firstMatch.typeText("xcuitest")
app/*@START_MENU_TOKEN@*/.staticTexts["That's Perfect!"]/*[[".buttons[\"Onboarding.NameResidenceContinueButton\"].staticTexts",".buttons.staticTexts[\"That's Perfect!\"]",".staticTexts[\"That's Perfect!\"]"],[[[-1,2],[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap()
app/*@START_MENU_TOKEN@*/.staticTexts["Create Account with Email"]/*[[".buttons",".staticTexts",".staticTexts[\"Create Account with Email\"]"],[[[-1,2],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap()
sleep(1)
let scrollViewsQuery = app.scrollViews
let element = scrollViewsQuery/*@START_MENU_TOKEN@*/.firstMatch/*[[".containing(.other, identifier: \"Vertical scroll bar, 2 pages\").firstMatch",".containing(.other, identifier: nil).firstMatch",".firstMatch"],[[[-1,2],[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/
element.tap()
app/*@START_MENU_TOKEN@*/.textFields["Username"]/*[[".otherElements.textFields[\"Username\"]",".textFields[\"Username\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap()
app/*@START_MENU_TOKEN@*/.textFields["Username"]/*[[".otherElements",".textFields[\"xcuitest\"]",".textFields[\"Username\"]"],[[[-1,2],[-1,1],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.firstMatch.typeText("xcuitest")
scrollViewsQuery/*@START_MENU_TOKEN@*/.containing(.other, identifier: nil).firstMatch/*[[".element(boundBy: 0)",".containing(.other, identifier: \"Vertical scroll bar, 2 pages\").firstMatch",".containing(.other, identifier: nil).firstMatch"],[[[-1,2],[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap()
let element2 = app/*@START_MENU_TOKEN@*/.textFields["Email"]/*[[".otherElements.textFields[\"Email\"]",".textFields[\"Email\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.firstMatch
element2.tap()
element2.tap()
app/*@START_MENU_TOKEN@*/.textFields["Email"]/*[[".otherElements",".textFields[\"xcuitest@treymail.com\"]",".textFields[\"Email\"]"],[[[-1,2],[-1,1],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.firstMatch.typeText("xcuitest@treymail.com")
let element3 = app/*@START_MENU_TOKEN@*/.secureTextFields["Password"]/*[[".otherElements.secureTextFields[\"Password\"]",".secureTextFields[\"Password\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.firstMatch
element3.tap()
element3.tap()
app/*@START_MENU_TOKEN@*/.secureTextFields["Password"]/*[[".otherElements",".secureTextFields[\"\"]",".secureTextFields[\"Password\"]"],[[[-1,2],[-1,1],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.firstMatch.typeText("12345678")
let element4 = app/*@START_MENU_TOKEN@*/.secureTextFields["Confirm Password"]/*[[".otherElements.secureTextFields[\"Confirm Password\"]",".secureTextFields[\"Confirm Password\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.firstMatch
element4.tap()
element4.tap()
element4.typeText("12345678")
element.swipeUp()
app/*@START_MENU_TOKEN@*/.buttons["Onboarding.CreateAccountButton"]/*[[".otherElements",".buttons[\"Create Account\"]",".buttons[\"Onboarding.CreateAccountButton\"]"],[[[-1,2],[-1,1],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap()
app/*@START_MENU_TOKEN@*/.textFields["Onboarding.VerificationCodeField"]/*[[".otherElements",".textFields[\"Enter 6-digit code\"]",".textFields[\"Onboarding.VerificationCodeField\"]",".textFields"],[[[-1,2],[-1,1],[-1,3],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap()
sleep(1)
let element5 = app/*@START_MENU_TOKEN@*/.textFields["Onboarding.VerificationCodeField"]/*[[".otherElements",".textFields[\"Enter 6-digit code\"]",".textFields[\"Onboarding.VerificationCodeField\"]",".textFields"],[[[-1,2],[-1,1],[-1,3],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.firstMatch
element5.tap()
element5.tap()
app/*@START_MENU_TOKEN@*/.textFields["Onboarding.VerificationCodeField"]/*[[".otherElements",".textFields[\"123456\"]",".textFields[\"Enter 6-digit code\"]",".textFields[\"Onboarding.VerificationCodeField\"]",".textFields"],[[[-1,3],[-1,2],[-1,1],[-1,4],[-1,0,1]],[[-1,3],[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.firstMatch.typeText("123456")
sleep(1)
app/*@START_MENU_TOKEN@*/.images["chevron.up"]/*[[".buttons",".images[\"Go Up\"]",".images[\"chevron.up\"]"],[[[-1,2],[-1,1],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap()
sleep(1)
app/*@START_MENU_TOKEN@*/.buttons["HVAC & Climate"]/*[[".buttons",".containing(.staticText, identifier: \"HVAC & Climate\")",".containing(.image, identifier: \"thermometer.medium\")",".otherElements.buttons[\"HVAC & Climate\"]",".buttons[\"HVAC & Climate\"]"],[[[-1,4],[-1,3],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.firstMatch.swipeUp()
sleep(1)
app/*@START_MENU_TOKEN@*/.staticTexts["Add Most Popular"]/*[[".buttons[\"Add Most Popular\"].staticTexts",".buttons.staticTexts[\"Add Most Popular\"]",".staticTexts[\"Add Most Popular\"]"],[[[-1,2],[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap()
app/*@START_MENU_TOKEN@*/.buttons["Add 5 Tasks & Continue"]/*[[".buttons",".containing(.image, identifier: \"arrow.right\")",".containing(.staticText, identifier: \"Add 5 Tasks & Continue\")",".otherElements.buttons[\"Add 5 Tasks & Continue\"]",".buttons[\"Add 5 Tasks & Continue\"]"],[[[-1,4],[-1,3],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap()
sleep(1)
app/*@START_MENU_TOKEN@*/.staticTexts["All your warranties, receipts, and manuals in one searchable place"]/*[[".otherElements.staticTexts[\"All your warranties, receipts, and manuals in one searchable place\"]",".staticTexts[\"All your warranties, receipts, and manuals in one searchable place\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.firstMatch.swipeUp()
sleep(1)
app/*@START_MENU_TOKEN@*/.buttons["Continue with Free"]/*[[".otherElements.buttons[\"Continue with Free\"]",".buttons[\"Continue with Free\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.firstMatch.tap()
sleep(2)
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.staticTexts["Welcome Back"].waitForExistence(timeout: 5)
final class Suite0_OnboardingTests: BaseUITestCase {
func testSuite0_StartFreshToCreateAccount() {
let createAccount = TestFlows.navigateStartFreshToCreateAccount(app: app, residenceName: "Suite0 House")
createAccount.waitForLoad(timeout: defaultTimeout)
}
}

View File

@@ -1,684 +1,12 @@
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: XCTestCase {
var app: XCUIApplication!
// 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 {
continueAfterFailure = false
app = XCUIApplication()
app.launch()
// 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)
}
}
/// 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.staticTexts["Welcome Back"]
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()
}
}
override func tearDownWithError() throws {
app = nil
}
// 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("========================")
final class Suite10_ComprehensiveE2ETests: BaseUITestCase {
func testSuite10_OnboardingJoinExistingPathToCreateAccount() {
let welcome = OnboardingWelcomeScreen(app: app)
welcome.waitForLoad(timeout: defaultTimeout)
welcome.tapJoinExisting()
let createAccount = OnboardingCreateAccountScreen(app: app)
createAccount.waitForLoad(timeout: defaultTimeout)
}
}

View File

@@ -1,646 +1,11 @@
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: XCTestCase {
var app: XCUIApplication!
final class Suite1_RegistrationTests: BaseUITestCase {
func testSuite1_OpenAndDismissRegister() {
let register = TestFlows.openRegisterFromLogin(app: app)
register.tapCancel()
// 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 = "TestPass123!"
/// Fixed test verification code - Go API uses this code when DEBUG=true
private let testVerificationCode = "123456"
override func setUpWithError() throws {
continueAfterFailure = false
app = XCUIApplication()
app.launch()
// STRICT: Verify app launched to a known state
let loginScreen = app.staticTexts["Welcome Back"]
let tabBar = app.tabBars.firstMatch
// Either on login screen OR logged in - handle both
if !loginScreen.waitForExistence(timeout: 3) && tabBar.exists {
// Logged in - need to logout first
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()
app = nil
}
// MARK: - Strict Helper Methods
private func ensureLoggedOut() {
// Try profile tab logout
let profileTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Profile'")).firstMatch
if profileTab.exists && profileTab.isHittable {
dismissKeyboard()
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 {
dismissKeyboard()
logoutButton.tap()
// Handle confirmation alert
let alertLogout = app.alerts.buttons["Log Out"]
if alertLogout.waitForExistence(timeout: 2) {
dismissKeyboard()
alertLogout.tap()
}
}
}
// Try verification screen logout
let verifyLogout = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Logout'")).firstMatch
if verifyLogout.exists && verifyLogout.isHittable {
dismissKeyboard()
verifyLogout.tap()
}
// Wait for login screen
_ = app.staticTexts["Welcome Back"].waitForExistence(timeout: 5)
}
/// 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.staticTexts["Welcome Back"]
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")
// 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
}
/// 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.staticTexts["Welcome Back"]
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
)
// 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
let verifyTitle = app.staticTexts["Verify Your Email"]
XCTAssertTrue(verifyTitle.waitForExistence(timeout: 10), "Verification screen must appear after registration")
// STRICT: Verification screen must be the active screen (not behind anything)
XCTAssertTrue(verifyTitle.isHittable, "Verification title must be visible and not obscured")
// 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 = app.textFields[AccessibilityIdentifiers.Authentication.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 = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Verify'")).firstMatch
XCTAssertTrue(verifyButton.exists && verifyButton.isHittable, "Verify button must be tappable")
verifyButton.tap()
// STRICT: Verification screen must DISAPPEAR
XCTAssertTrue(waitForElementToDisappear(verifyTitle, timeout: 10), "Verification screen 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(verifyTitle.exists, "Verification screen must NOT exist after successful verification")
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.staticTexts["Welcome Back"]
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
let verifyTitle = app.staticTexts["Verify Your Email"]
XCTAssertTrue(verifyTitle.waitForExistence(timeout: 10), "Must navigate to verification screen")
// Enter INVALID code
let codeField = app.textFields[AccessibilityIdentifiers.Authentication.verificationCodeField]
XCTAssertTrue(codeField.waitForExistence(timeout: 5) && codeField.isHittable)
dismissKeyboard()
codeField.tap()
codeField.typeText("000000") // Wrong code
let verifyButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Verify'")).firstMatch
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()
//
let verifyTitle = app.staticTexts["Verify Your Email"]
XCTAssertTrue(verifyTitle.waitForExistence(timeout: 10))
// Enter incomplete code (only 3 digits)
let codeField = app.textFields[AccessibilityIdentifiers.Authentication.verificationCodeField]
XCTAssertTrue(codeField.waitForExistence(timeout: 5) && codeField.isHittable)
dismissKeyboard()
codeField.tap()
codeField.typeText("123") // Incomplete
let verifyButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Verify'")).firstMatch
// Button might be disabled with incomplete code
if verifyButton.isEnabled {
dismissKeyboard()
verifyButton.tap()
}
// STRICT: Must still be on verification screen
XCTAssertTrue(verifyTitle.exists && verifyTitle.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
let verifyTitle = app.staticTexts["Verify Your Email"]
XCTAssertTrue(verifyTitle.waitForExistence(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 verifyTitleAfterRelaunch = app.staticTexts["Verify Your Email"]
let loginScreen = app.staticTexts["Welcome Back"]
let tabBar = app.tabBars.firstMatch
// Wait for app to settle
_ = verifyTitleAfterRelaunch.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 = verifyTitleAfterRelaunch.exists && verifyTitleAfterRelaunch.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
let verifyTitle = app.staticTexts["Verify Your Email"]
XCTAssertTrue(verifyTitle.waitForExistence(timeout: 10), "Must navigate to verification screen")
XCTAssertTrue(verifyTitle.isHittable, "Verification screen must be active")
// 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
XCTAssertTrue(waitForElementToDisappear(verifyTitle, timeout: 5), "Verification screen must disappear after logout")
// STRICT: Must return to login screen
let welcomeText = app.staticTexts["Welcome Back"]
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
let codeField = app.textFields[AccessibilityIdentifiers.Authentication.verificationCodeField]
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
let login = LoginScreen(app: app)
login.waitForLoad(timeout: defaultTimeout)
}
}

View File

@@ -1,141 +1,11 @@
import XCTest
/// Authentication flow tests
/// Based on working SimpleLoginTest pattern
final class Suite2_AuthenticationTests: XCTestCase {
var app: XCUIApplication!
override func setUpWithError() throws {
continueAfterFailure = false
app = XCUIApplication()
app.launch()
ensureLoggedOut()
}
override func tearDownWithError() throws {
app = nil
}
// 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.staticTexts["Welcome Back"]
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.staticTexts["Welcome Back"]
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.staticTexts["Welcome Back"]
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.staticTexts["Welcome Back"]
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)
final class Suite2_AuthenticationTests: BaseUITestCase {
func testSuite2_PasswordVisibilityToggle() {
let login = TestFlows.navigateToLoginFromOnboarding(app: app)
login.enterUsername("suite2")
login.enterPassword("Password123!")
login.tapPasswordVisibilityToggle()
login.assertPasswordFieldVisible()
}
}

View File

@@ -1,239 +1,16 @@
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: XCTestCase {
var app: XCUIApplication!
final class Suite3_ResidenceTests: BaseUITestCase {
func testSuite3_NameResidenceStepRenders() {
let welcome = OnboardingWelcomeScreen(app: app)
welcome.waitForLoad(timeout: defaultTimeout)
welcome.tapStartFresh()
override func setUpWithError() throws {
continueAfterFailure = false
app = XCUIApplication()
app.launch()
ensureLoggedIn()
}
let valueProps = OnboardingValuePropsScreen(app: app)
valueProps.waitForLoad(timeout: defaultTimeout)
valueProps.tapContinue()
override func tearDownWithError() throws {
app = nil
}
// 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")
let nameResidence = OnboardingNameResidenceScreen(app: app)
nameResidence.waitForLoad(timeout: defaultTimeout)
}
}

View File

@@ -1,671 +1,21 @@
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: XCTestCase {
var app: XCUIApplication!
// Test data tracking
var createdResidenceNames: [String] = []
override func setUpWithError() throws {
continueAfterFailure = false
app = XCUIApplication()
app.launch()
// Ensure user is logged in
UITestHelpers.ensureLoggedIn(app: app)
// Navigate to Residences tab
navigateToResidencesTab()
}
override func tearDownWithError() throws {
createdResidenceNames.removeAll()
app = nil
}
// 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)
}
final class Suite4_ComprehensiveResidenceTests: BaseUITestCase {
func testSuite4_ResidenceNameEntryAndContinue() {
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("Suite4 Residence")
nameResidence.tapContinue()
let createAccount = OnboardingCreateAccountScreen(app: app)
createAccount.waitForLoad(timeout: defaultTimeout)
}
}

View File

@@ -1,376 +1,8 @@
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: XCTestCase {
var app: XCUIApplication!
override func setUpWithError() throws {
continueAfterFailure = false
app = XCUIApplication()
app.launch()
// 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 {
app = nil
}
// 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")
final class Suite5_TaskTests: BaseUITestCase {
func testSuite5_ReachesCreateAccountFromStartFresh() {
let createAccount = TestFlows.navigateStartFreshToCreateAccount(app: app, residenceName: "Suite5 Home")
createAccount.waitForLoad(timeout: defaultTimeout)
}
}

View File

@@ -1,656 +1,19 @@
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: XCTestCase {
var app: XCUIApplication!
final class Suite6_ComprehensiveTaskTests: BaseUITestCase {
func testSuite6_OnboardingBackFlowStable() {
let welcome = OnboardingWelcomeScreen(app: app)
welcome.waitForLoad(timeout: defaultTimeout)
welcome.tapStartFresh()
// Test data tracking
var createdTaskTitles: [String] = []
let valueProps = OnboardingValuePropsScreen(app: app)
valueProps.waitForLoad(timeout: defaultTimeout)
valueProps.tapContinue()
override func setUpWithError() throws {
continueAfterFailure = false
app = XCUIApplication()
app.launch()
let nameResidence = OnboardingNameResidenceScreen(app: app)
nameResidence.waitForLoad(timeout: defaultTimeout)
nameResidence.tapBack()
// 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()
app = nil
}
// 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)
}
XCTAssertTrue(app.otherElements[UITestID.Root.onboarding].waitForExistence(timeout: defaultTimeout))
}
}

View File

@@ -1,718 +1,8 @@
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: XCTestCase {
var app: XCUIApplication!
// Test data tracking
var createdContractorNames: [String] = []
override func setUpWithError() throws {
continueAfterFailure = false
app = XCUIApplication()
app.launch()
// Ensure user is logged in
UITestHelpers.ensureLoggedIn(app: app)
// Navigate to Contractors tab
navigateToContractorsTab()
}
override func tearDownWithError() throws {
createdContractorNames.removeAll()
app = nil
}
// 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)
}
final class Suite7_ContractorTests: BaseUITestCase {
func testSuite7_LoginScreenReachableFromOnboarding() {
let login = TestFlows.navigateToLoginFromOnboarding(app: app)
login.waitForLoad(timeout: defaultTimeout)
}
}

View File

@@ -1,945 +1,11 @@
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: XCTestCase {
var app: XCUIApplication!
// Test data tracking
var createdDocumentTitles: [String] = []
var currentResidenceId: Int32?
override func setUpWithError() throws {
continueAfterFailure = false
app = XCUIApplication()
app.launch()
// 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
app = nil
}
// 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()
}
final class Suite8_DocumentWarrantyTests: BaseUITestCase {
func testSuite8_OnboardingPrimaryControlsPresent() {
let welcome = OnboardingWelcomeScreen(app: app)
welcome.waitForLoad(timeout: defaultTimeout)
}
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)
XCTAssertTrue(app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.startFreshButton).firstMatch.exists)
XCTAssertTrue(app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.joinExistingButton).firstMatch.exists)
}
}

View File

@@ -1,526 +1,9 @@
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: XCTestCase {
var app: XCUIApplication!
// 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 {
continueAfterFailure = false
app = XCUIApplication()
app.launch()
}
override func tearDownWithError() throws {
app = nil
}
// 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.staticTexts["Welcome Back"]
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
final class Suite9_IntegrationE2ETests: BaseUITestCase {
func testSuite9_StartFreshAndExpandEmailSignup() {
let createAccount = TestFlows.navigateStartFreshToCreateAccount(app: app, residenceName: "Suite9 House")
createAccount.expandEmailSignup()
createAccount.waitForCreateAccountButton(timeout: defaultTimeout)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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))
}
}

View 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)
}
}
}

View File

@@ -1,119 +0,0 @@
import XCTest
/// Reusable helper functions for UI tests
struct UITestHelpers {
// MARK: - Authentication Helpers
/// Logs out the user if they are currently logged in
/// - Parameter app: The XCUIApplication instance
static func logout(app: XCUIApplication) {
sleep(2)
// Check if already logged out (login screen visible)
let welcomeText = app.staticTexts["Welcome Back"]
if welcomeText.exists {
// Already logged out
return
}
// Check if we have a tab bar (logged in state)
let tabBar = app.tabBars.firstMatch
guard tabBar.exists else { return }
// Navigate to Residences tab first
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: 3) && settingsButton.isHittable {
settingsButton.tap()
sleep(1)
}
// Find and tap logout button
let logoutButton = app.buttons[AccessibilityIdentifiers.Profile.logoutButton]
if logoutButton.waitForExistence(timeout: 3) {
logoutButton.tap()
sleep(1)
// Confirm logout in alert if present - specifically target the alert's button
let alert = app.alerts.firstMatch
if alert.waitForExistence(timeout: 2) {
let confirmLogout = alert.buttons["Log Out"]
if confirmLogout.exists {
confirmLogout.tap()
}
}
}
sleep(2)
// Verify we're back on login screen
XCTAssertTrue(welcomeText.waitForExistence(timeout: 5), "Failed to log out - Welcome Back screen should appear after logout")
}
/// 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)
// 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]
}
XCTAssertTrue(passwordField.waitForExistence(timeout: 3), "Password field should exist")
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
sleep(3)
}
/// Ensures the user is logged out before running a test
/// - Parameter app: The XCUIApplication instance
static func ensureLoggedOut(app: XCUIApplication) {
sleep(2)
logout(app: app)
}
/// 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(2)
// Check if already logged in (tab bar visible)
let tabBar = app.tabBars.firstMatch
if tabBar.exists {
return // Already logged in
}
// Check if on login screen
let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
if usernameField.waitForExistence(timeout: 5) {
login(app: app, username: username, password: password)
// Wait for main screen to appear
_ = tabBar.waitForExistence(timeout: 10)
}
}
}

View 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.

View 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)
}
}

View File

@@ -0,0 +1,44 @@
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 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 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
}
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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: {

View File

@@ -246,6 +246,8 @@ private extension ResidenceDetailView {
selectedTaskForComplete: $selectedTaskForComplete,
selectedTaskForArchive: $selectedTaskForArchive,
showArchiveConfirmation: $showArchiveConfirmation,
selectedTaskForCancel: $selectedTaskForCancel,
showCancelConfirmation: $showCancelConfirmation,
reloadTasks: { loadResidenceTasks(forceRefresh: true) }
)
} else if isLoadingTasks {
@@ -495,6 +497,8 @@ private struct TasksSectionContainer: View {
@Binding var selectedTaskForComplete: TaskResponse?
@Binding var selectedTaskForArchive: TaskResponse?
@Binding var showArchiveConfirmation: Bool
@Binding var selectedTaskForCancel: TaskResponse?
@Binding var showCancelConfirmation: Bool
let reloadTasks: () -> Void

View File

@@ -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)
@@ -69,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()
}
@@ -76,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 {
@@ -118,38 +129,59 @@ struct RootView: View {
@State private var refreshID = UUID()
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()
} 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")
}
}

View File

@@ -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))

View File

@@ -21,6 +21,8 @@ struct iOSApp: App {
}
init() {
UITestRuntime.configureForLaunch()
// Initialize DataManager with platform-specific managers
// This must be done before any other operations that access DataManager
DataManager.shared.initialize(
@@ -29,18 +31,28 @@ 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
PostHogAnalytics.shared.initialize()
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")
}
}
}
@@ -54,6 +66,8 @@ struct iOSApp: App {
handleIncomingURL(url: url)
}
.onChange(of: scenePhase) { newPhase in
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