# XCUITest Implementation Guide ## Overview This guide provides step-by-step instructions for implementing comprehensive UI testing for the MyCrib iOS app using XCUITest. ## Table of Contents 1. [Project Setup](#project-setup) 2. [Adding Accessibility Identifiers](#adding-accessibility-identifiers) 3. [Running the Tests](#running-the-tests) 4. [Continuous Integration](#continuous-integration) --- ## Project Setup ### Current Status ✅ **Already Done:** - UI Test target exists: `MyCribTests` - Base test infrastructure in place (`TestHelpers.swift`, `BaseUITest`) - Initial test files created ### What's New: 1. **Centralized Accessibility Identifiers**: `iosApp/Helpers/AccessibilityIdentifiers.swift` 2. **Comprehensive Test Suite**: Based on `AUTOMATED_TEST_EXECUTION_PLAN.md` 3. **Enhanced Test Utilities**: Improved helpers for common operations --- ## Adding Accessibility Identifiers ### Step 1: Import the Identifiers File Ensure all view files import the identifiers: ```swift import SwiftUI // No import needed - same module ``` ### Step 2: Add Identifiers to Views For each interactive element in your views, add the `.accessibilityIdentifier()` modifier. #### Example: LoginView.swift ```swift // Username Field TextField("Enter your email", text: $viewModel.username) .textInputAutocapitalization(.never) .autocorrectionDisabled() .keyboardType(.emailAddress) .accessibilityIdentifier(AccessibilityIdentifiers.Authentication.usernameField) // Password Field SecureField("Enter your password", text: $viewModel.password) .accessibilityIdentifier(AccessibilityIdentifiers.Authentication.passwordField) // Login Button Button(action: viewModel.login) { Text("Sign In") } .accessibilityIdentifier(AccessibilityIdentifiers.Authentication.loginButton) // Sign Up Button Button("Sign Up") { showingRegister = true } .accessibilityIdentifier(AccessibilityIdentifiers.Authentication.signUpButton) ``` #### Example: RegisterView.swift ```swift TextField("Username", text: $viewModel.username) .accessibilityIdentifier(AccessibilityIdentifiers.Authentication.registerUsernameField) TextField("Email", text: $viewModel.email) .accessibilityIdentifier(AccessibilityIdentifiers.Authentication.registerEmailField) SecureField("Password", text: $viewModel.password) .accessibilityIdentifier(AccessibilityIdentifiers.Authentication.registerPasswordField) SecureField("Confirm Password", text: $viewModel.confirmPassword) .accessibilityIdentifier(AccessibilityIdentifiers.Authentication.registerConfirmPasswordField) Button("Register") { viewModel.register() } .accessibilityIdentifier(AccessibilityIdentifiers.Authentication.registerButton) ``` #### Example: MainTabView.swift ```swift TabView(selection: $selectedTab) { NavigationView { ResidencesListView() } .tabItem { Label("Residences", systemImage: "house.fill") } .tag(0) .accessibilityIdentifier(AccessibilityIdentifiers.Navigation.residencesTab) // Repeat for other tabs... } ``` #### Example: ResidenceFormView.swift ```swift TextField("Property Name", text: $name) .accessibilityIdentifier(AccessibilityIdentifiers.Residence.nameField) Picker("Property Type", selection: $selectedPropertyType) { // ... } .accessibilityIdentifier(AccessibilityIdentifiers.Residence.propertyTypePicker) TextField("Street Address", text: $streetAddress) .accessibilityIdentifier(AccessibilityIdentifiers.Residence.streetAddressField) TextField("City", text: $city) .accessibilityIdentifier(AccessibilityIdentifiers.Residence.cityField) TextField("State/Province", text: $stateProvince) .accessibilityIdentifier(AccessibilityIdentifiers.Residence.stateProvinceField) TextField("Postal Code", text: $postalCode) .accessibilityIdentifier(AccessibilityIdentifiers.Residence.postalCodeField) TextField("Country", text: $country) .accessibilityIdentifier(AccessibilityIdentifiers.Residence.countryField) TextField("Bedrooms", text: $bedrooms) .accessibilityIdentifier(AccessibilityIdentifiers.Residence.bedroomsField) TextField("Bathrooms", text: $bathrooms) .accessibilityIdentifier(AccessibilityIdentifiers.Residence.bathroomsField) Toggle("Primary Residence", isOn: $isPrimary) .accessibilityIdentifier(AccessibilityIdentifiers.Residence.isPrimaryToggle) Button("Save") { saveResidence() } .accessibilityIdentifier(AccessibilityIdentifiers.Residence.saveButton) ``` #### Example: TaskFormView.swift ```swift TextField("Title", text: $title) .accessibilityIdentifier(AccessibilityIdentifiers.Task.titleField) TextField("Description", text: $description) .accessibilityIdentifier(AccessibilityIdentifiers.Task.descriptionField) Picker("Category", selection: $selectedCategory) { // ... } .accessibilityIdentifier(AccessibilityIdentifiers.Task.categoryPicker) Picker("Frequency", selection: $selectedFrequency) { // ... } .accessibilityIdentifier(AccessibilityIdentifiers.Task.frequencyPicker) Picker("Priority", selection: $selectedPriority) { // ... } .accessibilityIdentifier(AccessibilityIdentifiers.Task.priorityPicker) DatePicker("Due Date", selection: $dueDate) .accessibilityIdentifier(AccessibilityIdentifiers.Task.dueDatePicker) TextField("Estimated Cost", text: $estimatedCost) .accessibilityIdentifier(AccessibilityIdentifiers.Task.estimatedCostField) Button("Save") { saveTask() } .accessibilityIdentifier(AccessibilityIdentifiers.Task.saveButton) ``` #### Example: ResidenceDetailView.swift ```swift // Add Task Button (FAB or toolbar button) Button(action: { showingAddTask = true }) { Image(systemName: "plus") } .accessibilityIdentifier(AccessibilityIdentifiers.Residence.addTaskButton) // Edit Button Button("Edit") { showingEditForm = true } .accessibilityIdentifier(AccessibilityIdentifiers.Residence.editButton) // Delete Button Button("Delete", role: .destructive) { showingDeleteConfirmation = true } .accessibilityIdentifier(AccessibilityIdentifiers.Residence.deleteButton) ``` ### Step 3: Dynamic Identifiers for List Items For list items (residence cards, task cards, etc.), use dynamic identifiers: ```swift ForEach(residences, id: \.id) { residence in ResidenceCard(residence: residence) .accessibilityIdentifier("\(AccessibilityIdentifiers.Residence.residenceCard).\(residence.id)") .onTapGesture { selectedResidence = residence } } ``` ### Quick Reference: Files to Update Here's a checklist of all views that need accessibility identifiers: **Authentication (Priority: High)** - ✅ `/iosApp/Login/LoginView.swift` - Partially done - ⬜ `/iosApp/Register/RegisterView.swift` - ⬜ `/iosApp/VerifyEmail/VerifyEmailView.swift` - ⬜ `/iosApp/PasswordReset/ForgotPasswordView.swift` **Navigation (Priority: High)** - ⬜ `/iosApp/MainTabView.swift` - ⬜ `/iosApp/ContentView.swift` **Residence (Priority: High)** - ⬜ `/iosApp/ResidenceFormView.swift` - ⬜ `/iosApp/Residence/ResidencesListView.swift` - ⬜ `/iosApp/Residence/ResidenceDetailView.swift` - ⬜ `/iosApp/AddResidenceView.swift` - ⬜ `/iosApp/EditResidenceView.swift` **Task (Priority: High)** - ⬜ `/iosApp/Task/TaskFormView.swift` - ⬜ `/iosApp/Task/AddTaskView.swift` - ⬜ `/iosApp/Task/EditTaskView.swift` - ⬜ `/iosApp/Task/AllTasksView.swift` - ⬜ `/iosApp/Task/CompleteTaskView.swift` **Contractor (Priority: Medium)** - ⬜ `/iosApp/Contractor/ContractorsListView.swift` - ⬜ `/iosApp/Contractor/ContractorDetailView.swift` **Document (Priority: Medium)** - ⬜ `/iosApp/Documents/DocumentsWarrantiesView.swift` - ⬜ `/iosApp/Documents/AddDocumentView.swift` - ⬜ `/iosApp/Documents/DocumentDetailView.swift` **Profile (Priority: Medium)** - ⬜ `/iosApp/Profile/ProfileView.swift` - ⬜ `/iosApp/Profile/ProfileTabView.swift` --- ## Running the Tests ### In Xcode 1. **Open the project:** ```bash cd /Users/treyt/Desktop/code/MyCrib/MyCribKMM/iosApp open iosApp.xcodeproj ``` 2. **Select the test target:** - Product → Scheme → MyCribTests 3. **Choose a simulator:** - iPhone 15 Pro (recommended) - iOS 17.0+ 4. **Run all tests:** - Press `⌘ + U` (Command + U) - Or: Product → Test 5. **Run specific test:** - Open test file (e.g., `AuthenticationUITests.swift`) - Click the diamond icon next to the test method - Or: Right-click → Run Test ### From Command Line ```bash # Navigate to iOS app directory cd /Users/treyt/Desktop/code/MyCrib/MyCribKMM/iosApp # Run all tests xcodebuild test \ -project iosApp.xcodeproj \ -scheme MyCribTests \ -destination 'platform=iOS Simulator,name=iPhone 15 Pro,OS=17.0' # Run specific test class xcodebuild test \ -project iosApp.xcodeproj \ -scheme MyCribTests \ -destination 'platform=iOS Simulator,name=iPhone 15 Pro,OS=17.0' \ -only-testing:MyCribTests/AuthenticationUITests # Run specific test method xcodebuild test \ -project iosApp.xcodeproj \ -scheme MyCribTests \ -destination 'platform=iOS Simulator,name=iPhone 15 Pro,OS=17.0' \ -only-testing:MyCribTests/AuthenticationUITests/testLoginWithValidCredentials ``` ### Test Results Test results are saved to: ``` ~/Library/Developer/Xcode/DerivedData/iosApp-*/Logs/Test/ ``` --- ## Continuous Integration ### GitHub Actions Example Create `.github/workflows/ios-tests.yml`: ```yaml name: iOS UI Tests on: push: branches: [ main, develop ] pull_request: branches: [ main, develop ] jobs: test: runs-on: macos-14 # macOS Sonoma with Xcode 15+ timeout-minutes: 30 steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Xcode uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: '15.2' - name: Start Django Backend run: | cd myCribAPI docker-compose up -d sleep 10 # Wait for backend to start - name: Check Backend Health run: | curl --retry 5 --retry-delay 3 http://localhost:8000/api/ - name: Run iOS UI Tests run: | cd MyCribKMM/iosApp xcodebuild test \ -project iosApp.xcodeproj \ -scheme MyCribTests \ -destination 'platform=iOS Simulator,name=iPhone 15 Pro,OS=17.2' \ -resultBundlePath TestResults.xcresult \ -enableCodeCoverage YES - name: Upload Test Results if: always() uses: actions/upload-artifact@v4 with: name: test-results path: MyCribKMM/iosApp/TestResults.xcresult - name: Upload Screenshots if: failure() uses: actions/upload-artifact@v4 with: name: screenshots path: ~/Library/Developer/CoreSimulator/Devices/*/data/tmp/ if-no-files-found: ignore - name: Stop Docker Containers if: always() run: | cd myCribAPI docker-compose down ``` --- ##Test Configuration ### Test Plan (Optional) Create a `.xctestplan` file for better organization: 1. In Xcode: File → New → Test Plan 2. Name it: `MyCribTestPlan.xctestplan` 3. Configure: - **Configurations**: Debug, Release - **Test Targets**: MyCribTests - **Code Coverage**: Enable - **Screenshots**: Automatically on failure ### Launch Arguments Configure launch arguments for testing: ```swift // In test setUp() app.launchArguments = [ "--uitesting", // Flag for UI testing mode "--disable-animations", // Speed up tests "--reset-user-defaults", // Clean state "--use-test-api" // Point to test backend ] app.launch() ``` Handle in your app: ```swift // In AppDelegate or App init if ProcessInfo.processInfo.arguments.contains("--uitesting") { // Disable animations UIView.setAnimationsEnabled(false) // Clear user defaults if ProcessInfo.processInfo.arguments.contains("--reset-user-defaults") { let domain = Bundle.main.bundleIdentifier! UserDefaults.standard.removePersistentDomain(forName: domain) } // Use test API if ProcessInfo.processInfo.arguments.contains("--use-test-api") { ApiConfig.CURRENT_ENV = .LOCAL } } ``` --- ## Best Practices ### 1. Write Maintainable Tests ```swift // ✅ Good: Descriptive test names func testUserCanLoginWithValidCredentials() { } // ❌ Bad: Vague test names func testLogin() { } ``` ### 2. Use Page Object Pattern ```swift // Create a LoginPage helper struct LoginPage { let app: XCUIApplication var usernameField: XCUIElement { app.textFields[AccessibilityIdentifiers.Authentication.usernameField] } var passwordField: XCUIElement { app.secureTextFields[AccessibilityIdentifiers.Authentication.passwordField] } var loginButton: XCUIElement { app.buttons[AccessibilityIdentifiers.Authentication.loginButton] } func login(username: String, password: String) { usernameField.tap() usernameField.typeText(username) passwordField.tap() passwordField.typeText(password) loginButton.tap() } } // Use in tests func testLogin() { let loginPage = LoginPage(app: app) loginPage.login(username: "testuser", password: "password") // Assert... } ``` ### 3. Wait for Elements ```swift // ✅ Good: Wait with timeout let loginButton = app.buttons["Login"] XCTAssertTrue(loginButton.waitForExistence(timeout: 5)) // ❌ Bad: Assume immediate availability XCTAssertTrue(app.buttons["Login"].exists) ``` ### 4. Clean State Between Tests ```swift override func tearDown() { // Logout if logged in if app.tabBars.exists { logout() } super.tearDown() } ``` ### 5. Use Meaningful Assertions ```swift // ✅ Good: Clear assertion messages XCTAssertTrue( app.tabBars.exists, "Should navigate to main tab view after login" ) // ❌ Bad: No context XCTAssertTrue(app.tabBars.exists) ``` --- ## Troubleshooting ### Common Issues **Issue: Elements not found** - **Solution**: Verify accessibility identifier is added to the view - **Debug**: Use `app.debugDescription` to see all elements **Issue: Tests are flaky** - **Solution**: Add waits (`waitForExistence`) before interactions - **Solution**: Disable animations **Issue: Tests run slow** - **Solution**: Use `--disable-animations` launch argument - **Solution**: Run tests in parallel (Xcode 13+) **Issue: Backend not ready** - **Solution**: Add health check before tests - **Solution**: Increase wait time in pre-flight checks ### Debugging Tips ```swift // Print all elements print(app.debugDescription) // Take screenshot manually let screenshot = XCUIScreen.main.screenshot() let attachment = XCTAttachment(screenshot: screenshot) attachment.lifetime = .keepAlways add(attachment) // Breakpoint in test // Use Xcode's test navigator to pause at failures ``` --- ## Next Steps 1. ✅ Add accessibility identifiers to all views (see checklist above) 2. ✅ Run existing tests to verify setup 3. ✅ Review and expand test coverage using the new comprehensive test suite 4. ⬜ Set up CI/CD pipeline 5. ⬜ Configure test reporting/dashboards --- ## Resources - [XCUITest Documentation](https://developer.apple.com/documentation/xctest/user_interface_tests) - [WWDC: UI Testing in Xcode](https://developer.apple.com/videos/play/wwdc2019/413/) - [Test Plan Configuration](https://developer.apple.com/documentation/xcode/organizing-tests-to-improve-feedback) - [Accessibility for UIKit](https://developer.apple.com/documentation/uikit/accessibility)