import XCTest // MARK: - Task Screens /// Page object for the task list screen (kanban or list view). struct TaskListScreen { let app: XCUIApplication var addButton: XCUIElement { let byID = app.buttons[AccessibilityIdentifiers.Task.addButton].firstMatch if byID.exists { return byID } // Fallback: nav bar plus/Add button let navBarButtons = app.navigationBars.buttons for i in 0.. XCUIElement { let predicate = NSPredicate(format: "label CONTAINS %@", title) let match = app.staticTexts.containing(predicate).firstMatch // If found immediately, return if match.waitForExistence(timeout: 3) { return match } // Scroll through kanban columns (swipe left up to 6 times) let scrollView = app.scrollViews.firstMatch guard scrollView.exists else { return match } for _ in 0..<6 { scrollView.swipeLeft() if match.waitForExistence(timeout: 1) { return match } } // Scroll back and try right direction for _ in 0..<6 { scrollView.swipeRight() if match.waitForExistence(timeout: 1) { return match } } return match } } /// Page object for the task create/edit form. struct TaskFormScreen { let app: XCUIApplication var titleField: XCUIElement { app.textFields[AccessibilityIdentifiers.Task.titleField] } var descriptionField: XCUIElement { app.textViews[AccessibilityIdentifiers.Task.descriptionField] } var saveButton: XCUIElement { app.buttons[AccessibilityIdentifiers.Task.saveButton] } var cancelButton: XCUIElement { app.buttons[AccessibilityIdentifiers.Task.formCancelButton] } func waitForLoad(timeout: TimeInterval = 15) { XCTAssertTrue(titleField.waitForExistence(timeout: timeout), "Expected task form to load") } func enterTitle(_ text: String) { titleField.waitForExistenceOrFail(timeout: 10) titleField.focusAndType(text, app: app) } func enterDescription(_ text: String) { app.swipeUp() if descriptionField.waitForExistence(timeout: 5) { descriptionField.focusAndType(text, app: app) } } func save() { KeyboardDismisser.dismiss(app: app) // Scroll the form so any focused-field state commits before the // submit action reads it. Without this the title binding can lag. app.swipeUp() saveButton.waitForExistenceOrFail(timeout: 10) saveButton.forceTap() _ = saveButton.waitForNonExistence(timeout: 15) } func cancel() { cancelButton.waitForExistenceOrFail(timeout: 10) cancelButton.forceTap() } } // MARK: - Contractor Screens /// Page object for the contractor list screen. struct ContractorListScreen { let app: XCUIApplication var addButton: XCUIElement { let byID = app.buttons[AccessibilityIdentifiers.Contractor.addButton] if byID.exists { return byID } let navBarButtons = app.navigationBars.buttons for i in 0.. XCUIElement { app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] %@", name)).firstMatch } } /// Page object for the contractor create/edit form. struct ContractorFormScreen { let app: XCUIApplication var nameField: XCUIElement { app.textFields[AccessibilityIdentifiers.Contractor.nameField] } var phoneField: XCUIElement { app.textFields[AccessibilityIdentifiers.Contractor.phoneField] } var emailField: XCUIElement { app.textFields[AccessibilityIdentifiers.Contractor.emailField] } var companyField: XCUIElement { app.textFields[AccessibilityIdentifiers.Contractor.companyField] } var saveButton: XCUIElement { app.buttons[AccessibilityIdentifiers.Contractor.saveButton] } var cancelButton: XCUIElement { app.buttons[AccessibilityIdentifiers.Contractor.formCancelButton] } func waitForLoad(timeout: TimeInterval = 15) { XCTAssertTrue(nameField.waitForExistence(timeout: timeout), "Expected contractor form to load") } func enterName(_ text: String) { nameField.waitForExistenceOrFail(timeout: 10) nameField.focusAndType(text, app: app) } func enterPhone(_ text: String) { if phoneField.waitForExistence(timeout: 5) { phoneField.focusAndType(text, app: app) } } func enterEmail(_ text: String) { if emailField.waitForExistence(timeout: 5) { emailField.focusAndType(text, app: app) } } func enterCompany(_ text: String) { if companyField.waitForExistence(timeout: 5) { companyField.focusAndType(text, app: app) } } func save() { KeyboardDismisser.dismiss(app: app) if !saveButton.exists || !saveButton.isHittable { app.swipeUp() } saveButton.waitForExistenceOrFail(timeout: 10) saveButton.forceTap() _ = saveButton.waitForNonExistence(timeout: 15) } func cancel() { cancelButton.waitForExistenceOrFail(timeout: 10) cancelButton.forceTap() } } /// Page object for the contractor detail screen. struct ContractorDetailScreen { let app: XCUIApplication var menuButton: XCUIElement { let byID = app.buttons[AccessibilityIdentifiers.Contractor.menuButton] if byID.exists { return byID } return app.images["ellipsis.circle"].firstMatch } var editButton: XCUIElement { app.buttons[AccessibilityIdentifiers.Contractor.editButton] } var deleteButton: XCUIElement { app.buttons[AccessibilityIdentifiers.Contractor.deleteButton] } func waitForLoad(timeout: TimeInterval = 15) { let deadline = Date().addingTimeInterval(timeout) var loaded = false repeat { loaded = menuButton.exists || app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Phone' OR label CONTAINS[c] 'Email'")).firstMatch.exists if loaded { break } RunLoop.current.run(until: Date().addingTimeInterval(0.2)) } while Date() < deadline XCTAssertTrue(loaded, "Expected contractor detail screen to load") } func openMenu() { menuButton.waitForExistenceOrFail(timeout: 10) menuButton.forceTap() } func tapEdit() { openMenu() editButton.waitForExistenceOrFail(timeout: 10) editButton.forceTap() } func tapDelete() { openMenu() deleteButton.waitForExistenceOrFail(timeout: 10) deleteButton.forceTap() } } // MARK: - Document Screens /// Page object for the document list screen. struct DocumentListScreen { let app: XCUIApplication var addButton: XCUIElement { let byID = app.buttons[AccessibilityIdentifiers.Document.addButton] if byID.exists { return byID } let navBarButtons = app.navigationBars.buttons for i in 0.. XCUIElement { app.staticTexts.containing(NSPredicate(format: "label CONTAINS %@", title)).firstMatch } } /// Page object for the document create/edit form. struct DocumentFormScreen { let app: XCUIApplication var titleField: XCUIElement { app.textFields[AccessibilityIdentifiers.Document.titleField] } var residencePicker: XCUIElement { app.buttons[AccessibilityIdentifiers.Document.residencePicker] } var saveButton: XCUIElement { app.buttons[AccessibilityIdentifiers.Document.saveButton] } var cancelButton: XCUIElement { app.buttons[AccessibilityIdentifiers.Document.formCancelButton] } func waitForLoad(timeout: TimeInterval = 15) { XCTAssertTrue(titleField.waitForExistence(timeout: timeout), "Expected document form to load") } func enterTitle(_ text: String) { titleField.waitForExistenceOrFail(timeout: 10) titleField.focusAndType(text, app: app) } /// Selects a residence by name from the picker. Returns true if selection succeeded. @discardableResult func selectResidence(name: String) -> Bool { guard residencePicker.waitForExistence(timeout: 5) else { return false } residencePicker.tap() let menuItem = app.menuItems.firstMatch if menuItem.waitForExistence(timeout: 5) { // Look for matching item first let matchingItem = app.menuItems.containing(NSPredicate(format: "label CONTAINS[c] %@", name)).firstMatch if matchingItem.exists { matchingItem.tap() return true } // Fallback: tap last available item let allItems = app.menuItems.allElementsBoundByIndex if let last = allItems.last { last.tap() return true } } // Dismiss picker if nothing found app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.9)).tap() return false } func save() { KeyboardDismisser.dismiss(app: app) // Unconditional swipe-up matches the task form fix — forces SwiftUI // state to commit before the submit button reads it. app.swipeUp() saveButton.waitForExistenceOrFail(timeout: 10) if saveButton.isHittable { saveButton.tap() } else { saveButton.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap() } _ = saveButton.waitForNonExistence(timeout: 15) } func cancel() { cancelButton.waitForExistenceOrFail(timeout: 10) cancelButton.forceTap() } } // MARK: - Residence Detail Screen /// Page object for the residence detail screen. struct ResidenceDetailScreen { let app: XCUIApplication var editButton: XCUIElement { app.buttons[AccessibilityIdentifiers.Residence.editButton] } var deleteButton: XCUIElement { app.buttons[AccessibilityIdentifiers.Residence.deleteButton] } var shareButton: XCUIElement { app.buttons[AccessibilityIdentifiers.Residence.shareButton] } func waitForLoad(timeout: TimeInterval = 15) { let deadline = Date().addingTimeInterval(timeout) var loaded = false repeat { loaded = editButton.exists || app.otherElements[AccessibilityIdentifiers.Residence.detailView].exists || app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks' OR label CONTAINS[c] 'Maintenance'")).firstMatch.exists if loaded { break } RunLoop.current.run(until: Date().addingTimeInterval(0.2)) } while Date() < deadline XCTAssertTrue(loaded, "Expected residence detail screen to load") } func tapEdit() { editButton.waitForExistenceOrFail(timeout: 10) editButton.forceTap() } func tapDelete() { deleteButton.waitForExistenceOrFail(timeout: 10) deleteButton.forceTap() } func tapShare() { shareButton.waitForExistenceOrFail(timeout: 10) shareButton.forceTap() } }