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