import XCTest /// Comprehensive contractor UI test suite. /// /// Merges the former `Suite7_ContractorTests` (broad create/edit/view/persist /// coverage and edge cases) with `ContractorIntegrationTests` (CON-002/005/006 /// CRUD against the real backend). /// /// Per-test isolation: `AuthenticatedUITestCase` mints a fresh account, logs in, /// and deletes it in teardown. Contractors do NOT require a residence, so /// pure-create tests need no preconditions. The edit/delete tests that operate /// on an EXISTING contractor seed it in `seedAccountPreconditions` (before /// login) so the app loads it on its post-login fetch. final class ContractorUITests: AuthenticatedUITestCase { // MARK: - Preconditions /// Contractors seeded before login for the edit/delete integration tests. /// A fresh account is empty at login, so anything these tests need to see /// must be seeded here (before login) rather than in the test body. private(set) var editTargetContractor: TestContractor? private(set) var deleteTargetContractor: TestContractor? override func seedAccountPreconditions(_ account: TestAccount) { super.seedAccountPreconditions(account) // CON-005 edits an existing contractor; CON-006 deletes one. editTargetContractor = account.seedContractor( name: "Edit Target Contractor \(Int(Date().timeIntervalSince1970))" ) deleteTargetContractor = account.seedContractor( name: "Delete Contractor \(Int(Date().timeIntervalSince1970))" ) } // MARK: - Page Objects private var contractorList: ContractorListScreen { ContractorListScreen(app: app) } private var contractorForm: ContractorFormScreen { ContractorFormScreen(app: app) } private var contractorDetail: ContractorDetailScreen { ContractorDetailScreen(app: app) } // MARK: - Helper Methods private func openContractorForm(file: StaticString = #filePath, line: UInt = #line) { let addButton = contractorList.addButton addButton.waitForExistenceOrFail(timeout: defaultTimeout, message: "Contractor add button should exist", file: file, line: line) addButton.tap() contractorForm.nameField.waitForExistenceOrFail(timeout: defaultTimeout, message: "Contractor form should open", file: file, line: line) } private func selectSpecialty(specialty: String) { let specialtyPicker = app.buttons[AccessibilityIdentifiers.Contractor.specialtyPicker].firstMatch guard specialtyPicker.waitForExistence(timeout: defaultTimeout) else { return } specialtyPicker.tap() // Specialty picker is a sheet with checkboxes let option = app.staticTexts[specialty] if option.waitForExistence(timeout: navigationTimeout) { option.tap() } // Dismiss the sheet by tapping Done let doneButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Done'")).firstMatch if doneButton.waitForExistence(timeout: defaultTimeout) { doneButton.tap() } } private func createContractor( name: String, phone: String? = nil, email: String? = nil, company: String? = nil, specialty: String? = nil ) { openContractorForm() contractorForm.enterName(name) dismissKeyboard() if let phone = phone { fillTextField(identifier: AccessibilityIdentifiers.Contractor.phoneField, text: phone) dismissKeyboard() } if let email = email { fillTextField(identifier: AccessibilityIdentifiers.Contractor.emailField, text: email) dismissKeyboard() } if let company = company { fillTextField(identifier: AccessibilityIdentifiers.Contractor.companyField, text: company) dismissKeyboard() } if let specialty = specialty { selectSpecialty(specialty: specialty) } app.swipeUp() let submitButton = contractorForm.saveButton submitButton.waitForExistenceOrFail(timeout: defaultTimeout, message: "Save button should exist") submitButton.tap() _ = submitButton.waitForNonExistence(timeout: navigationTimeout) // Navigate to contractors tab to trigger list refresh and reset scroll position navigateToContractors() } private func findContractor(name: String, scrollIfNeeded: Bool = true) -> XCUIElement { let element = contractorList.findContractor(name: name) if element.exists && element.isHittable { return element } guard scrollIfNeeded else { return element } let scrollView = app.scrollViews.firstMatch guard scrollView.exists else { return element } scrollView.swipeDown(velocity: .fast) usleep(30_000) var lastVisibleRow = "" for _ in 0.. 0 ? count - 1 : 0).tap() } } // Wait for the detail view to dismiss and return to list _ = detailView.waitForNonExistence(timeout: loginTimeout) // Pull to refresh in case the list didn't auto-update pullToRefresh() // Verify the contractor is no longer visible let deletedContractor = app.staticTexts[deleteName] XCTAssertTrue( deletedContractor.waitForNonExistence(timeout: loginTimeout), "Deleted contractor should no longer appear" ) } }