Files
honeyDueKMP/iosApp/HoneyDueUITests/Framework/TestDataSeeder.swift
Trey T 4df8707b92 UI test infrastructure overhaul — 58% to 96% pass rate (231/241)
Major infrastructure changes:
- BaseUITestCase: per-suite app termination via class setUp() prevents
  stale state when parallel clones share simulators
- relaunchBetweenTests override for suites that modify login/onboarding state
- focusAndType: dedicated SecureTextField path handles iOS strong password
  autofill suggestions (Choose My Own Password / Not Now dialogs)
- LoginScreenObject: tapSignUp/tapForgotPassword use scrollIntoView for
  offscreen buttons instead of simple swipeUp
- Removed all coordinate taps from ForgotPasswordScreen, VerifyResetCodeScreen,
  ResetPasswordScreen (Rule 3 compliance)
- Removed all usleep calls from screen objects (Rule 14 compliance)

App fixes exposed by tests:
- ContractorsListView: added onDismiss to sheet for list refresh after save
- AllTasksView: added Task.RefreshButton accessibility identifier
- AccessibilityIdentifiers: added Task.refreshButton
- DocumentsWarrantiesView: onDismiss handler for document list refresh
- Various form views: textContentType, submitLabel, onSubmit for keyboard flow

Test fixes:
- PasswordResetTests: handle auto-login after reset (app skips success screen)
- AuthenticatedUITestCase: refreshTasks() helper for kanban toolbar button
- All pre-login suites use relaunchBetweenTests for test independence
- Deleted dead code: AuthenticatedTestCase, SeededTestData, SeedTests,
  CleanupTests, old Suite0/2/3, Suite1_RegistrationRebuildTests

10 remaining failures: 5 iOS strong password autofill (simulator env),
3 pull-to-refresh gesture on empty lists, 2 feature coverage edge cases.

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

236 lines
8.3 KiB
Swift

import Foundation
import XCTest
/// Seeds backend data for integration tests via API calls.
///
/// All methods require a valid auth token from a `TestSession`.
/// Created resources are tracked so `TestDataCleaner` can remove them in teardown.
enum TestDataSeeder {
// MARK: - Residence Seeding
/// Create a residence with just a name. Returns the residence or fails the test.
@discardableResult
static func createResidence(
token: String,
name: String? = nil,
file: StaticString = #filePath,
line: UInt = #line
) -> TestResidence {
let residenceName = name ?? "Test Residence \(uniqueSuffix())"
guard let residence = TestAccountAPIClient.createResidence(token: token, name: residenceName) else {
XCTFail("Failed to seed residence '\(residenceName)'", file: file, line: line)
preconditionFailure("seeding failed — see XCTFail above")
}
return residence
}
/// Create a residence with address fields populated.
@discardableResult
static func createResidenceWithAddress(
token: String,
name: String? = nil,
street: String = "123 Test St",
city: String = "Testville",
state: String = "TX",
postalCode: String = "78701",
file: StaticString = #filePath,
line: UInt = #line
) -> TestResidence {
let residenceName = name ?? "Addressed Residence \(uniqueSuffix())"
guard let residence = TestAccountAPIClient.createResidence(
token: token,
name: residenceName,
fields: [
"street_address": street,
"city": city,
"state_province": state,
"postal_code": postalCode
]
) else {
XCTFail("Failed to seed residence with address '\(residenceName)'", file: file, line: line)
preconditionFailure("seeding failed — see XCTFail above")
}
return residence
}
// MARK: - Task Seeding
/// Create a task in a residence. Returns the task or fails the test.
@discardableResult
static func createTask(
token: String,
residenceId: Int,
title: String? = nil,
fields: [String: Any] = [:],
file: StaticString = #filePath,
line: UInt = #line
) -> TestTask {
let taskTitle = title ?? "Test Task \(uniqueSuffix())"
guard let task = TestAccountAPIClient.createTask(
token: token,
residenceId: residenceId,
title: taskTitle,
fields: fields
) else {
XCTFail("Failed to seed task '\(taskTitle)'", file: file, line: line)
preconditionFailure("seeding failed — see XCTFail above")
}
return task
}
/// Create a task with a due date.
@discardableResult
static func createTaskWithDueDate(
token: String,
residenceId: Int,
title: String? = nil,
daysFromNow: Int = 7,
file: StaticString = #filePath,
line: UInt = #line
) -> TestTask {
let dueDate = Calendar.current.date(byAdding: .day, value: daysFromNow, to: Date())!
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withFullDate]
let dueDateStr = formatter.string(from: dueDate)
return createTask(
token: token,
residenceId: residenceId,
title: title ?? "Due Task \(uniqueSuffix())",
fields: ["due_date": dueDateStr],
file: file,
line: line
)
}
/// Create a cancelled task (create then cancel via API).
@discardableResult
static func createCancelledTask(
token: String,
residenceId: Int,
title: String? = nil,
file: StaticString = #filePath,
line: UInt = #line
) -> TestTask {
let task = createTask(token: token, residenceId: residenceId, title: title ?? "Cancelled Task \(uniqueSuffix())", file: file, line: line)
guard let cancelled = TestAccountAPIClient.cancelTask(token: token, id: task.id) else {
XCTFail("Failed to cancel seeded task \(task.id)", file: file, line: line)
preconditionFailure("seeding failed — see XCTFail above")
}
return cancelled
}
// MARK: - Contractor Seeding
/// Create a contractor. Returns the contractor or fails the test.
@discardableResult
static func createContractor(
token: String,
name: String? = nil,
fields: [String: Any] = [:],
file: StaticString = #filePath,
line: UInt = #line
) -> TestContractor {
let contractorName = name ?? "Test Contractor \(uniqueSuffix())"
guard let contractor = TestAccountAPIClient.createContractor(
token: token,
name: contractorName,
fields: fields
) else {
XCTFail("Failed to seed contractor '\(contractorName)'", file: file, line: line)
preconditionFailure("seeding failed — see XCTFail above")
}
return contractor
}
/// Create a contractor with contact info.
@discardableResult
static func createContractorWithContact(
token: String,
name: String? = nil,
company: String = "Test Co",
phone: String = "555-0100",
email: String? = nil,
file: StaticString = #filePath,
line: UInt = #line
) -> TestContractor {
let contractorName = name ?? "Contact Contractor \(uniqueSuffix())"
let contactEmail = email ?? "\(uniqueSuffix())@contractor.test"
return createContractor(
token: token,
name: contractorName,
fields: ["company": company, "phone": phone, "email": contactEmail],
file: file,
line: line
)
}
// MARK: - Document Seeding
/// Create a document in a residence. Returns the document or fails the test.
@discardableResult
static func createDocument(
token: String,
residenceId: Int,
title: String? = nil,
documentType: String = "general",
fields: [String: Any] = [:],
file: StaticString = #filePath,
line: UInt = #line
) -> TestDocument {
let docTitle = title ?? "Test Doc \(uniqueSuffix())"
guard let document = TestAccountAPIClient.createDocument(
token: token,
residenceId: residenceId,
title: docTitle,
documentType: documentType,
fields: fields
) else {
XCTFail("Failed to seed document '\(docTitle)'", file: file, line: line)
preconditionFailure("seeding failed — see XCTFail above")
}
return document
}
// MARK: - Composite Scenarios
/// Create a residence with N tasks already in it. Returns (residence, [tasks]).
static func createResidenceWithTasks(
token: String,
residenceName: String? = nil,
taskCount: Int = 3,
file: StaticString = #filePath,
line: UInt = #line
) -> (residence: TestResidence, tasks: [TestTask]) {
let residence = createResidence(token: token, name: residenceName, file: file, line: line)
var tasks: [TestTask] = []
for i in 1...taskCount {
let task = createTask(token: token, residenceId: residence.id, title: "Task \(i) \(uniqueSuffix())", file: file, line: line)
tasks.append(task)
}
return (residence, tasks)
}
/// Create a residence with a contractor and a document. Returns all three.
static func createFullResidence(
token: String,
file: StaticString = #filePath,
line: UInt = #line
) -> (residence: TestResidence, task: TestTask, contractor: TestContractor, document: TestDocument) {
let residence = createResidence(token: token, file: file, line: line)
let task = createTask(token: token, residenceId: residence.id, file: file, line: line)
let contractor = createContractor(token: token, file: file, line: line)
let document = createDocument(token: token, residenceId: residence.id, file: file, line: line)
return (residence, task, contractor, document)
}
// MARK: - Private
private static func uniqueSuffix() -> String {
let stamp = Int(Date().timeIntervalSince1970) % 100000
let random = Int.random(in: 100...999)
return "\(stamp)_\(random)"
}
}