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>
236 lines
8.3 KiB
Swift
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)"
|
|
}
|
|
}
|