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>
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
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:
- Centralized Accessibility Identifiers:
iosApp/Helpers/AccessibilityIdentifiers.swift - Comprehensive Test Suite: Based on
AUTOMATED_TEST_EXECUTION_PLAN.md - 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
-
Open the project:
cd /Users/treyt/Desktop/code/MyCrib/MyCribKMM/iosApp open iosApp.xcodeproj -
Select the test target:
- Product → Scheme → MyCribTests
-
Choose a simulator:
- iPhone 15 Pro (recommended)
- iOS 17.0+
-
Run all tests:
- Press
⌘ + U(Command + U) - Or: Product → Test
- Press
-
Run specific test:
- Open test file (e.g.,
AuthenticationUITests.swift) - Click the diamond icon next to the test method
- Or: Right-click → Run Test
- Open test file (e.g.,
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:
- In Xcode: File → New → Test Plan
- Name it:
MyCribTestPlan.xctestplan - 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.debugDescriptionto see all elements
Issue: Tests are flaky
- Solution: Add waits (
waitForExistence) before interactions - Solution: Disable animations
Issue: Tests run slow
- Solution: Use
--disable-animationslaunch 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
- ✅ Add accessibility identifiers to all views (see checklist above)
- ✅ Run existing tests to verify setup
- ✅ Review and expand test coverage using the new comprehensive test suite
- ⬜ Set up CI/CD pipeline
- ⬜ Configure test reporting/dashboards