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>
600 lines
15 KiB
Markdown
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)
|
|
|