Files
honeyDueKMP/iosApp/HoneyDueUITests/PageObjects/Screens.swift
Trey T d545fd463c Fix 10 failing UI tests: kanban scroll, menu-based edit, form submit reliability
- Screens.swift: findTask() now scrolls through kanban columns (swipe left/right)
  to locate tasks rendered off-screen in LazyHGrid
- Suite5: test06/07 use refreshTasks() instead of pullToRefresh() (kanban is
  horizontal), add API call before navigate for server processing delay
- Suite6: test09 opens "Task actions" menu before tapping edit (no detail screen)
- Suite8: submitForm() uses coordinate-based keyboard dismiss, retry tap, and
  longer timeout; test22/23 re-navigate after creation and use waitForExistence

Test results: 141/143 passed (was 131/143). Remaining 2 failures are pre-existing
(Suite1 test11) and flaky/unrelated (Suite3 testR307).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 17:21:31 -05:00

470 lines
15 KiB
Swift

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..<navBarButtons.count {
let button = navBarButtons.element(boundBy: i)
if button.label == "plus" || button.label.contains("Add") {
if button.isEnabled { return button }
}
}
// Fallback: empty state add button
let emptyStateButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Add Task'")).firstMatch
if emptyStateButton.exists && emptyStateButton.isEnabled { return emptyStateButton }
return byID
}
var emptyState: XCUIElement {
app.otherElements[AccessibilityIdentifiers.Task.emptyStateView]
}
var tasksList: XCUIElement {
app.otherElements[AccessibilityIdentifiers.Task.tasksList]
}
func waitForLoad(timeout: TimeInterval = 15) {
let deadline = Date().addingTimeInterval(timeout)
var loaded = false
repeat {
loaded = addButton.exists
|| emptyState.exists
|| tasksList.exists
|| app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch.exists
if loaded { break }
RunLoop.current.run(until: Date().addingTimeInterval(0.2))
} while Date() < deadline
XCTAssertTrue(loaded, "Expected task list screen to load")
}
func openCreateTask() {
addButton.waitForExistenceOrFail(timeout: 10)
addButton.forceTap()
}
func findTask(title: String) -> 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() {
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..<navBarButtons.count {
let button = navBarButtons.element(boundBy: i)
if button.label == "plus" || button.label.contains("Add") {
if button.isEnabled { return button }
}
}
return app.buttons.containing(NSPredicate(format: "label CONTAINS 'plus'")).firstMatch
}
var emptyState: XCUIElement {
app.otherElements[AccessibilityIdentifiers.Contractor.emptyStateView]
}
var contractorsList: XCUIElement {
app.otherElements[AccessibilityIdentifiers.Contractor.contractorsList]
}
func waitForLoad(timeout: TimeInterval = 15) {
let deadline = Date().addingTimeInterval(timeout)
var loaded = false
repeat {
loaded = addButton.exists
|| emptyState.exists
|| contractorsList.exists
|| app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Contractors'")).firstMatch.exists
if loaded { break }
RunLoop.current.run(until: Date().addingTimeInterval(0.2))
} while Date() < deadline
XCTAssertTrue(loaded, "Expected contractor list screen to load")
}
func openCreateContractor() {
addButton.waitForExistenceOrFail(timeout: 10)
addButton.forceTap()
}
func findContractor(name: String) -> 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() {
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..<navBarButtons.count {
let button = navBarButtons.element(boundBy: i)
if button.label == "plus" || button.label.contains("Add") {
if button.isEnabled { return button }
}
}
return app.buttons.containing(NSPredicate(format: "label CONTAINS 'plus'")).firstMatch
}
var emptyState: XCUIElement {
app.otherElements[AccessibilityIdentifiers.Document.emptyStateView]
}
var documentsList: XCUIElement {
app.otherElements[AccessibilityIdentifiers.Document.documentsList]
}
func waitForLoad(timeout: TimeInterval = 15) {
let deadline = Date().addingTimeInterval(timeout)
var loaded = false
repeat {
loaded = addButton.exists
|| emptyState.exists
|| documentsList.exists
|| app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Documents' OR label CONTAINS[c] 'Warranties'")).firstMatch.exists
if loaded { break }
RunLoop.current.run(until: Date().addingTimeInterval(0.2))
} while Date() < deadline
XCTAssertTrue(loaded, "Expected document list screen to load")
}
func openCreateDocument() {
addButton.waitForExistenceOrFail(timeout: 10)
addButton.forceTap()
}
func findDocument(title: String) -> 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() {
// Dismiss keyboard first
app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.15)).tap()
_ = app.keyboards.firstMatch.waitForNonExistence(timeout: 3)
if !saveButton.exists || !saveButton.isHittable {
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()
}
}