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

600 lines
15 KiB
Markdown

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