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>
This commit is contained in:
treyt
2026-03-15 17:32:13 -05:00
parent cf2e6d8bcc
commit 5c360a2796
57 changed files with 3781 additions and 928 deletions

View File

@@ -0,0 +1,145 @@
import XCTest
/// Post-suite cleanup that runs after all other test suites.
///
/// Alphabetically `SuiteZZ` sorts after all `Suite0``Suite10` and `Tests/` classes,
/// so this runs last in the test plan. It calls the admin API to wipe all test
/// data, leaving the database clean for the next run.
///
/// If the admin panel account isn't set up, cleanup is skipped (not failed).
final class SuiteZZ_CleanupTests: XCTestCase {
/// Admin panel credentials (separate from regular user auth).
/// Default: admin@honeydue.com / password123 (seeded via `./dev.sh seed-admin`)
private static let adminEmail = "admin@honeydue.com"
private static let adminPassword = "password123"
override func setUpWithError() throws {
try super.setUpWithError()
continueAfterFailure = true // Don't abort if cleanup partially fails
}
func test01_cleanupAllTestData() throws {
guard TestAccountAPIClient.isBackendReachable() else {
throw XCTSkip("Backend not reachable — skipping cleanup")
}
// Login to admin panel
guard let adminToken = loginToAdminPanel() else {
throw XCTSkip("Could not login to admin panel — is the admin user seeded? Run: ./dev.sh seed-admin")
}
// Call clear-all-data
let result = clearAllData(token: adminToken)
XCTAssertTrue(result.success, "Clear-all-data should succeed: \(result.message)")
if result.success {
print("[Cleanup] Deleted \(result.usersDeleted) users, preserved \(result.preserved) superadmins")
}
}
func test02_reseedBaselineData() throws {
guard TestAccountAPIClient.isBackendReachable() else {
throw XCTSkip("Backend not reachable — skipping re-seed")
}
// Re-create the testuser and admin accounts so the DB is ready
// for the next test run without needing Suite00.
let testUser = SeededTestData.TestUser.self
if let session = TestAccountAPIClient.createVerifiedAccount(
username: testUser.username,
email: testUser.email,
password: testUser.password
) {
SeededTestData.testUserToken = session.token
print("[Cleanup] Re-seeded testuser account")
}
let admin = SeededTestData.AdminUser.self
if let session = TestAccountAPIClient.createVerifiedAccount(
username: admin.username,
email: admin.email,
password: admin.password
) {
SeededTestData.adminUserToken = session.token
print("[Cleanup] Re-seeded admin account")
}
}
// MARK: - Admin API Helpers
private struct AdminLoginResponse: Decodable {
let token: String
}
private struct ClearResult {
let success: Bool
let usersDeleted: Int
let preserved: Int
let message: String
}
private func loginToAdminPanel() -> String? {
let url = URL(string: "\(TestAccountAPIClient.baseURL.replacingOccurrences(of: "/api", with: ""))/api/admin/auth/login")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try? JSONSerialization.data(withJSONObject: [
"email": Self.adminEmail,
"password": Self.adminPassword
])
request.timeoutInterval = 10
var result: String?
let semaphore = DispatchSemaphore(value: 0)
URLSession.shared.dataTask(with: request) { data, response, _ in
defer { semaphore.signal() }
guard let data = data,
let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode),
let decoded = try? JSONDecoder().decode(AdminLoginResponse.self, from: data) else {
return
}
result = decoded.token
}.resume()
semaphore.wait()
return result
}
private func clearAllData(token: String) -> ClearResult {
let url = URL(string: "\(TestAccountAPIClient.baseURL.replacingOccurrences(of: "/api", with: ""))/api/admin/settings/clear-all-data")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.timeoutInterval = 30
var clearResult = ClearResult(success: false, usersDeleted: 0, preserved: 0, message: "No response")
let semaphore = DispatchSemaphore(value: 0)
URLSession.shared.dataTask(with: request) { data, response, error in
defer { semaphore.signal() }
guard let data = data,
let httpResponse = response as? HTTPURLResponse else {
clearResult = ClearResult(success: false, usersDeleted: 0, preserved: 0,
message: error?.localizedDescription ?? "No response")
return
}
if (200...299).contains(httpResponse.statusCode),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
clearResult = ClearResult(
success: true,
usersDeleted: json["users_deleted"] as? Int ?? 0,
preserved: json["preserved_users"] as? Int ?? 0,
message: json["message"] as? String ?? "OK"
)
} else {
let body = String(data: data, encoding: .utf8) ?? "?"
clearResult = ClearResult(success: false, usersDeleted: 0, preserved: 0,
message: "HTTP \(httpResponse.statusCode): \(body)")
}
}.resume()
semaphore.wait()
return clearResult
}
}