Files
honeyDueKMP/iosApp/XCUITEST_IMPLEMENTATION_GUIDE.md
Trey t 74a474007b Add project documentation and test setup guides
Added comprehensive documentation for the KMM project structure, build
commands, and UI testing setup/troubleshooting.

Documentation added:
- CLAUDE.md: Complete KMM project guide for Claude Code with architecture,
  build commands, common tasks, and development patterns
- iosApp/UI_TESTS_*.md: UI testing strategy, implementation guides, summaries
- iosApp/XCUITEST_*.md: XCUITest implementation and debugging guides
- iosApp/TEST_FAILURES_ANALYSIS.md: Analysis of common test failures
- iosApp/ACCESSIBILITY_IDENTIFIERS_FIX.md: Guide for fixing accessibility issues
- iosApp/FIX_TEST_TARGET*.md: Guides for fixing test target configuration
- iosApp/fix_test_target.sh: Script to automate test target setup

The CLAUDE.md serves as the primary documentation for working with this
repository, providing quick access to build commands, architecture overview,
and common development tasks for both iOS and Android platforms.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 23:07:14 -06:00

15 KiB

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
  2. Adding Accessibility Identifiers
  3. Running the Tests
  4. 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:

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

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

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

TabView(selection: $selectedTab) {
    NavigationView {
        ResidencesListView()
    }
    .tabItem {
        Label("Residences", systemImage: "house.fill")
    }
    .tag(0)
    .accessibilityIdentifier(AccessibilityIdentifiers.Navigation.residencesTab)

    // Repeat for other tabs...
}

Example: ResidenceFormView.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

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

// 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:

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:

    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

# 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:

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:

// 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:

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

// ✅ Good: Descriptive test names
func testUserCanLoginWithValidCredentials() { }

// ❌ Bad: Vague test names
func testLogin() { }

2. Use Page Object Pattern

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

// ✅ 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

override func tearDown() {
    // Logout if logged in
    if app.tabBars.exists {
        logout()
    }

    super.tearDown()
}

5. Use Meaningful Assertions

// ✅ 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

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