Files
honeyDueKMP/iosApp/HoneyDueUITests/Framework/TestDataCleaner.swift
treyt 5c360a2796 Rearchitect UI test suite for complete, non-flaky coverage against live API
- Migrate Suite4-10, SmokeTests, NavigationCriticalPathTests to AuthenticatedTestCase
  with seeded admin account and real backend login
- Add 34 accessibility identifiers across 11 app views (task completion, profile,
  notifications, theme, join residence, manage users, forms)
- Create FeatureCoverageTests (14 tests) covering previously untested features:
  profile edit, theme selection, notification prefs, task completion, manage users,
  join residence, task templates
- Create MultiUserSharingTests (18 API tests) and MultiUserSharingUITests (8 XCUI
  tests) for full cross-user residence sharing lifecycle
- Add cleanup infrastructure: SuiteZZ_CleanupTests auto-wipes test data after runs,
  cleanup_test_data.sh script for manual reset via admin API
- Add share code API methods to TestAccountAPIClient (generateShareCode, joinWithCode,
  getShareCode, listResidenceUsers, removeUser)
- Fix app bugs found by tests:
  - ResidencesListView join callback now uses forceRefresh:true
  - APILayer invalidates task cache when residence count changes
  - AllTasksView auto-reloads tasks when residence list changes
- Fix test quality: keyboard focus waits, Save/Add button label matching,
  Documents tab label (Docs), remove API verification from UI tests
- DataLayerTests and PasswordResetTests now verify through UI, not API calls

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 17:32:13 -05:00

131 lines
4.6 KiB
Swift

import Foundation
import XCTest
/// Tracks and cleans up resources created during integration tests.
///
/// Usage:
/// ```
/// let cleaner = TestDataCleaner(token: session.token)
/// let residence = TestDataSeeder.createResidence(token: session.token)
/// cleaner.trackResidence(residence.id)
/// // ... test runs ...
/// cleaner.cleanAll() // called in tearDown
/// ```
class TestDataCleaner {
private let token: String
private var residenceIds: [Int] = []
private var taskIds: [Int] = []
private var contractorIds: [Int] = []
private var documentIds: [Int] = []
init(token: String) {
self.token = token
}
// MARK: - Track Resources
func trackResidence(_ id: Int) {
residenceIds.append(id)
}
func trackTask(_ id: Int) {
taskIds.append(id)
}
func trackContractor(_ id: Int) {
contractorIds.append(id)
}
func trackDocument(_ id: Int) {
documentIds.append(id)
}
// MARK: - Seed + Track (Convenience)
/// Create a residence and automatically track it for cleanup.
@discardableResult
func seedResidence(name: String? = nil) -> TestResidence {
let residence = TestDataSeeder.createResidence(token: token, name: name)
trackResidence(residence.id)
return residence
}
/// Create a task and automatically track it for cleanup.
@discardableResult
func seedTask(residenceId: Int, title: String? = nil, fields: [String: Any] = [:]) -> TestTask {
let task = TestDataSeeder.createTask(token: token, residenceId: residenceId, title: title, fields: fields)
trackTask(task.id)
return task
}
/// Create a contractor and automatically track it for cleanup.
@discardableResult
func seedContractor(name: String? = nil, fields: [String: Any] = [:]) -> TestContractor {
let contractor = TestDataSeeder.createContractor(token: token, name: name, fields: fields)
trackContractor(contractor.id)
return contractor
}
/// Create a document and automatically track it for cleanup.
@discardableResult
func seedDocument(residenceId: Int, title: String? = nil, documentType: String = "general") -> TestDocument {
let document = TestDataSeeder.createDocument(token: token, residenceId: residenceId, title: title, documentType: documentType)
trackDocument(document.id)
return document
}
/// Create a residence with tasks, all tracked for cleanup.
func seedResidenceWithTasks(residenceName: String? = nil, taskCount: Int = 3) -> (residence: TestResidence, tasks: [TestTask]) {
let result = TestDataSeeder.createResidenceWithTasks(token: token, residenceName: residenceName, taskCount: taskCount)
trackResidence(result.residence.id)
result.tasks.forEach { trackTask($0.id) }
return result
}
/// Create a full residence with task, contractor, and document, all tracked.
func seedFullResidence() -> (residence: TestResidence, task: TestTask, contractor: TestContractor, document: TestDocument) {
let result = TestDataSeeder.createFullResidence(token: token)
trackResidence(result.residence.id)
trackTask(result.task.id)
trackContractor(result.contractor.id)
trackDocument(result.document.id)
return result
}
// MARK: - Cleanup
/// Delete all tracked resources in reverse dependency order.
/// Documents and tasks first (they depend on residences), then contractors, then residences.
/// Failures are logged but don't fail the test cleanup is best-effort.
func cleanAll() {
// Delete documents first (depend on residences)
for id in documentIds.reversed() {
_ = TestAccountAPIClient.deleteDocument(token: token, id: id)
}
documentIds.removeAll()
// Delete tasks (depend on residences)
for id in taskIds.reversed() {
_ = TestAccountAPIClient.deleteTask(token: token, id: id)
}
taskIds.removeAll()
// Delete contractors (independent, but clean before residences)
for id in contractorIds.reversed() {
_ = TestAccountAPIClient.deleteContractor(token: token, id: id)
}
contractorIds.removeAll()
// Delete residences last
for id in residenceIds.reversed() {
_ = TestAccountAPIClient.deleteResidence(token: token, id: id)
}
residenceIds.removeAll()
}
/// Number of tracked resources (for debugging).
var trackedCount: Int {
residenceIds.count + taskIds.count + contractorIds.count + documentIds.count
}
}