import XCTest /// Integration tests for contractor CRUD against the real local backend. /// /// Test Plan IDs: CON-002, CON-005, CON-006 /// Data is seeded via API and cleaned up in tearDown. final class ContractorIntegrationTests: AuthenticatedTestCase { override var useSeededAccount: Bool { true } // MARK: - CON-002: Create Contractor func testCON002_CreateContractorMinimalFields() { navigateToContractors() let addButton = app.buttons[AccessibilityIdentifiers.Contractor.addButton] let emptyState = app.otherElements[AccessibilityIdentifiers.Contractor.emptyStateView] let contractorList = app.otherElements[AccessibilityIdentifiers.Contractor.contractorsList] let loaded = addButton.waitForExistence(timeout: defaultTimeout) || emptyState.waitForExistence(timeout: 3) || contractorList.waitForExistence(timeout: 3) XCTAssertTrue(loaded, "Contractors screen should load") if addButton.exists && addButton.isHittable { addButton.forceTap() } else { let emptyAddButton = app.buttons.containing( NSPredicate(format: "label CONTAINS[c] 'Add' OR label CONTAINS[c] 'Create'") ).firstMatch emptyAddButton.waitForExistenceOrFail(timeout: defaultTimeout) emptyAddButton.forceTap() } let nameField = app.textFields[AccessibilityIdentifiers.Contractor.nameField] nameField.waitForExistenceOrFail(timeout: defaultTimeout) let uniqueName = "IntTest Contractor \(Int(Date().timeIntervalSince1970))" nameField.forceTap() nameField.typeText(uniqueName) let saveButton = app.buttons[AccessibilityIdentifiers.Contractor.saveButton] saveButton.scrollIntoView(in: app.scrollViews.firstMatch) saveButton.forceTap() let newContractor = app.staticTexts[uniqueName] XCTAssertTrue( newContractor.waitForExistence(timeout: longTimeout), "Newly created contractor should appear in list" ) } // MARK: - CON-005: Edit Contractor func testCON005_EditContractor() { // Seed a contractor via API let contractor = cleaner.seedContractor(name: "Edit Target Contractor \(Int(Date().timeIntervalSince1970))") navigateToContractors() // Find and tap the seeded contractor let card = app.staticTexts[contractor.name] card.waitForExistenceOrFail(timeout: longTimeout) card.forceTap() // Tap edit let editButton = app.buttons[AccessibilityIdentifiers.Contractor.editButton] editButton.waitForExistenceOrFail(timeout: defaultTimeout) editButton.forceTap() // Update name let nameField = app.textFields[AccessibilityIdentifiers.Contractor.nameField] nameField.waitForExistenceOrFail(timeout: defaultTimeout) nameField.forceTap() nameField.press(forDuration: 1.0) let selectAll = app.menuItems["Select All"] if selectAll.waitForExistence(timeout: 2) { selectAll.tap() } let updatedName = "Updated Contractor \(Int(Date().timeIntervalSince1970))" nameField.typeText(updatedName) let saveButton = app.buttons[AccessibilityIdentifiers.Contractor.saveButton] saveButton.scrollIntoView(in: app.scrollViews.firstMatch) saveButton.forceTap() let updatedText = app.staticTexts[updatedName] XCTAssertTrue( updatedText.waitForExistence(timeout: longTimeout), "Updated contractor name should appear after edit" ) } // MARK: - CON-007: Favorite Toggle func test20_toggleContractorFavorite() { // Seed a contractor via API and track it for cleanup let contractor = cleaner.seedContractor(name: "Favorite Toggle Contractor \(Int(Date().timeIntervalSince1970))") navigateToContractors() // Find and open the seeded contractor let card = app.staticTexts[contractor.name] card.waitForExistenceOrFail(timeout: longTimeout) card.forceTap() // Look for a favorite / star button in the detail view. // The button may be labelled "Favorite", carry a star SF symbol, or use a toggle. let favoriteButton = app.buttons.containing( NSPredicate(format: "label CONTAINS[c] 'Favorite' OR label CONTAINS[c] 'Star' OR label CONTAINS[c] 'favourite'") ).firstMatch guard favoriteButton.waitForExistence(timeout: defaultTimeout) else { XCTFail("Favorite/star button not found on contractor detail view") return } // Capture initial accessibility value / label to detect change let initialLabel = favoriteButton.label // First toggle — mark as favourite favoriteButton.forceTap() // Brief pause so the UI can settle after the API call _ = app.staticTexts.firstMatch.waitForExistence(timeout: 2) // The button's label or selected state should have changed let afterFirstToggleLabel = favoriteButton.label XCTAssertNotEqual( initialLabel, afterFirstToggleLabel, "Favorite button appearance should change after first toggle" ) // Second toggle — un-mark as favourite, state should return to original favoriteButton.forceTap() _ = app.staticTexts.firstMatch.waitForExistence(timeout: 2) let afterSecondToggleLabel = favoriteButton.label XCTAssertEqual( initialLabel, afterSecondToggleLabel, "Favorite button appearance should return to original after second toggle" ) } // MARK: - CON-008: Contractor by Residence Filter func test21_contractorByResidenceFilter() throws { // Seed a residence and a contractor linked to it let residence = cleaner.seedResidence(name: "Filter Test Residence \(Int(Date().timeIntervalSince1970))") let contractor = cleaner.seedContractor( name: "Residence Contractor \(Int(Date().timeIntervalSince1970))", fields: ["residence_id": residence.id] ) navigateToResidences() // Open the seeded residence's detail view let residenceText = app.staticTexts[residence.name] residenceText.waitForExistenceOrFail(timeout: longTimeout) residenceText.forceTap() // Look for a Contractors section within the residence detail. // The section header text or accessibility element is checked first. let contractorsSectionHeader = app.staticTexts.containing( NSPredicate(format: "label CONTAINS[c] 'Contractor'") ).firstMatch guard contractorsSectionHeader.waitForExistence(timeout: defaultTimeout) else { throw XCTSkip("Residence detail does not expose a Contractors section — skipping filter test") } // Verify the seeded contractor appears in the residence's contractor list let contractorEntry = app.staticTexts[contractor.name] XCTAssertTrue( contractorEntry.waitForExistence(timeout: defaultTimeout), "Contractor '\(contractor.name)' should appear in the contractors section of residence '\(residence.name)'" ) } // MARK: - CON-006: Delete Contractor func testCON006_DeleteContractor() { // Seed a contractor via API — don't track since we'll delete through UI let deleteName = "Delete Contractor \(Int(Date().timeIntervalSince1970))" TestDataSeeder.createContractor(token: session.token, name: deleteName) navigateToContractors() let target = app.staticTexts[deleteName] target.waitForExistenceOrFail(timeout: longTimeout) target.forceTap() let deleteButton = app.buttons[AccessibilityIdentifiers.Contractor.deleteButton] deleteButton.waitForExistenceOrFail(timeout: defaultTimeout) deleteButton.forceTap() let confirmButton = app.buttons[AccessibilityIdentifiers.Alert.confirmButton] let alertDelete = app.alerts.buttons.containing( NSPredicate(format: "label CONTAINS[c] 'Delete' OR label CONTAINS[c] 'Confirm'") ).firstMatch if confirmButton.waitForExistence(timeout: shortTimeout) { confirmButton.tap() } else if alertDelete.waitForExistence(timeout: shortTimeout) { alertDelete.tap() } let deletedContractor = app.staticTexts[deleteName] XCTAssertTrue( deletedContractor.waitForNonExistence(timeout: longTimeout), "Deleted contractor should no longer appear" ) } }