Files
honeyDueKMP/iosApp/XCUITEST_IMPLEMENTATION_GUIDE.md
Trey t 1e2adf7660 Rebrand from Casera/MyCrib to honeyDue
Total rebrand across KMM project:
- Kotlin package: com.example.casera -> com.tt.honeyDue (dirs + declarations)
- Gradle: rootProject.name, namespace, applicationId
- Android: manifest, strings.xml (all languages), widget resources
- iOS: pbxproj bundle IDs, Info.plist, entitlements, xcconfig
- iOS directories: Casera/ -> HoneyDue/, CaseraTests/ -> HoneyDueTests/, etc.
- Swift source: all class/struct/enum renames
- Deep links: casera:// -> honeydue://, .casera -> .honeydue
- App icons replaced with honeyDue honeycomb icon
- Domains: casera.treytartt.com -> honeyDue.treytartt.com
- Bundle IDs: com.tt.casera -> com.tt.honeyDue
- Database table names preserved

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 06:33:57 -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 HoneyDue 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: `HoneyDueTests`
- 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/HoneyDue/HoneyDueKMM/iosApp
open iosApp.xcodeproj
```
2. **Select the test target:**
- Product → Scheme → HoneyDueTests
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/HoneyDue/HoneyDueKMM/iosApp
# Run all tests
xcodebuild test \
-project iosApp.xcodeproj \
-scheme HoneyDueTests \
-destination 'platform=iOS Simulator,name=iPhone 15 Pro,OS=17.0'
# Run specific test class
xcodebuild test \
-project iosApp.xcodeproj \
-scheme HoneyDueTests \
-destination 'platform=iOS Simulator,name=iPhone 15 Pro,OS=17.0' \
-only-testing:HoneyDueTests/AuthenticationUITests
# Run specific test method
xcodebuild test \
-project iosApp.xcodeproj \
-scheme HoneyDueTests \
-destination 'platform=iOS Simulator,name=iPhone 15 Pro,OS=17.0' \
-only-testing:HoneyDueTests/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 honeyDueAPI
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 HoneyDueKMM/iosApp
xcodebuild test \
-project iosApp.xcodeproj \
-scheme HoneyDueTests \
-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: HoneyDueKMM/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 honeyDueAPI
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: `HoneyDueTestPlan.xctestplan`
3. Configure:
- **Configurations**: Debug, Release
- **Test Targets**: HoneyDueTests
- **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)